diff --git a/src/password_manager/entry_management.py b/src/password_manager/entry_management.py index b8c9bcb..87404a4 100644 --- a/src/password_manager/entry_management.py +++ b/src/password_manager/entry_management.py @@ -723,6 +723,93 @@ class EntryManager: entry_type = entry.get("type", entry.get("kind", EntryType.PASSWORD.value)) + provided_fields = { + "username": username, + "url": url, + "archived": archived, + "notes": notes, + "label": label, + "period": period, + "digits": digits, + "value": value, + "custom_fields": custom_fields, + "tags": tags, + } + + allowed = { + EntryType.PASSWORD.value: { + "username", + "url", + "label", + "archived", + "notes", + "custom_fields", + "tags", + }, + EntryType.TOTP.value: { + "label", + "period", + "digits", + "archived", + "notes", + "custom_fields", + "tags", + }, + EntryType.KEY_VALUE.value: { + "label", + "value", + "archived", + "notes", + "custom_fields", + "tags", + }, + EntryType.MANAGED_ACCOUNT.value: { + "label", + "value", + "archived", + "notes", + "custom_fields", + "tags", + }, + EntryType.SSH.value: { + "label", + "archived", + "notes", + "custom_fields", + "tags", + }, + EntryType.PGP.value: { + "label", + "archived", + "notes", + "custom_fields", + "tags", + }, + EntryType.NOSTR.value: { + "label", + "archived", + "notes", + "custom_fields", + "tags", + }, + EntryType.SEED.value: { + "label", + "archived", + "notes", + "custom_fields", + "tags", + }, + } + + allowed_fields = allowed.get(entry_type, set()) + invalid = { + k for k, v in provided_fields.items() if v is not None + } - allowed_fields + if invalid: + raise ValueError( + f"Entry type '{entry_type}' does not support fields: {', '.join(sorted(invalid))}" + ) + if entry_type == EntryType.TOTP.value: if label is not None: entry["label"] = label @@ -796,6 +883,7 @@ class EntryManager: print( colored(f"Error: Failed to modify entry at index {index}: {e}", "red") ) + raise def archive_entry(self, index: int) -> None: """Mark the specified entry as archived.""" diff --git a/src/seedpass/api.py b/src/seedpass/api.py index 916a162..85a23f1 100644 --- a/src/seedpass/api.py +++ b/src/seedpass/api.py @@ -207,16 +207,19 @@ def update_entry( """ _check_token(authorization) assert _pm is not None - _pm.entry_manager.modify_entry( - entry_id, - username=entry.get("username"), - url=entry.get("url"), - notes=entry.get("notes"), - label=entry.get("label"), - period=entry.get("period"), - digits=entry.get("digits"), - value=entry.get("value"), - ) + try: + _pm.entry_manager.modify_entry( + entry_id, + username=entry.get("username"), + url=entry.get("url"), + notes=entry.get("notes"), + label=entry.get("label"), + period=entry.get("period"), + digits=entry.get("digits"), + value=entry.get("value"), + ) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) return {"status": "ok"} diff --git a/src/seedpass/cli.py b/src/seedpass/cli.py index e7694b4..92f46a5 100644 --- a/src/seedpass/cli.py +++ b/src/seedpass/cli.py @@ -306,16 +306,20 @@ def entry_modify( ) -> None: """Modify an existing entry.""" pm = _get_pm(ctx) - pm.entry_manager.modify_entry( - entry_id, - username=username, - url=url, - notes=notes, - label=label, - period=period, - digits=digits, - value=value, - ) + try: + pm.entry_manager.modify_entry( + entry_id, + username=username, + url=url, + notes=notes, + label=label, + period=period, + digits=digits, + value=value, + ) + except ValueError as e: + typer.echo(str(e)) + raise typer.Exit(code=1) pm.sync_vault() diff --git a/src/tests/test_api_new_endpoints.py b/src/tests/test_api_new_endpoints.py index 758714c..e939048 100644 --- a/src/tests/test_api_new_endpoints.py +++ b/src/tests/test_api_new_endpoints.py @@ -93,6 +93,19 @@ def test_create_and_modify_ssh_entry(client): assert calls["modify"][1]["notes"] == "x" +def test_update_entry_error(client): + cl, token = client + + def modify(*a, **k): + raise ValueError("nope") + + api._pm.entry_manager.modify_entry = modify + headers = {"Authorization": f"Bearer {token}"} + res = cl.put("/api/v1/entry/1", json={"username": "x"}, headers=headers) + assert res.status_code == 400 + assert res.json() == {"detail": "nope"} + + def test_update_config_secret_mode(client): cl, token = client called = {} diff --git a/src/tests/test_modify_totp_entry.py b/src/tests/test_modify_totp_entry.py index b1cb825..8e038d6 100644 --- a/src/tests/test_modify_totp_entry.py +++ b/src/tests/test_modify_totp_entry.py @@ -1,4 +1,5 @@ from helpers import create_vault, TEST_SEED, TEST_PASSWORD +import pytest from password_manager.entry_management import EntryManager from password_manager.backup import BackupManager @@ -18,3 +19,14 @@ def test_modify_totp_entry_period_digits_and_archive(tmp_path): assert entry["period"] == 60 assert entry["digits"] == 8 assert entry["archived"] is True + + +def test_modify_totp_entry_invalid_field(tmp_path): + vault, _ = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD) + cfg_mgr = ConfigManager(vault, tmp_path) + backup_mgr = BackupManager(tmp_path, cfg_mgr) + em = EntryManager(vault, backup_mgr) + + em.add_totp("Example", TEST_SEED) + with pytest.raises(ValueError): + em.modify_entry(0, username="alice") diff --git a/src/tests/test_typer_cli.py b/src/tests/test_typer_cli.py index 90d4067..b64a2d1 100644 --- a/src/tests/test_typer_cli.py +++ b/src/tests/test_typer_cli.py @@ -396,6 +396,21 @@ def test_entry_modify(monkeypatch): assert called["args"][:5] == (1, "alice", None, None, None) +def test_entry_modify_invalid(monkeypatch): + def modify_entry(*a, **k): + raise ValueError("bad") + + pm = SimpleNamespace( + entry_manager=SimpleNamespace(modify_entry=modify_entry), + select_fingerprint=lambda fp: None, + sync_vault=lambda: None, + ) + monkeypatch.setattr(cli, "PasswordManager", lambda: pm) + result = runner.invoke(app, ["entry", "modify", "1", "--username", "alice"]) + assert result.exit_code == 1 + assert "bad" in result.stdout + + def test_entry_archive(monkeypatch): called = {}