feat(vaults): batch HTTP json vault writes with InsertKeys

This commit is contained in:
imSp4rky
2026-08-27 18:21:45 -06:00
parent 160ca063a6
commit e9ed05d4b2
3 changed files with 19 additions and 7 deletions
+1 -1
View File
@@ -167,7 +167,7 @@ A flexible HTTP vault that speaks one of three wire protocols, selected with `ap
password: "your-api-token" password: "your-api-token"
``` ```
Requests are `POST`ed as `{"method": ..., "params": ..., "token": ...}` using methods like `GetKey`, `InsertKey`, and `GetServices`. Bulk writes (`kv copy`, `kv add`) first try `InsertKeys` with `params: {"keys": {kid: key, ...}, "service": ..., "title": ...}` (up to 500 content keys per call) and expect `{"inserted": <count>}` back. A server that answers HTTP 404 to `InsertKeys` gets one `InsertKey` call per content key for the rest of the run instead. This mode cannot list all keys in bulk, so a `kv copy` from a json vault relies on the remote server's own duplicate handling. Requests are `POST`ed as `{"method": ..., "params": ..., "token": ...}` using methods like `GetKey`, `InsertKey`, and `GetServices`. Bulk writes (`kv copy`, `kv add`) first try `InsertKeys` with `params: {"keys": {kid: key, ...}, "service": ..., "title": ...}` (up to 500 content keys per call) and expect `{"inserted": <count>}` back. A server that answers HTTP 404 or `Unknown JSON-RPC method` to `InsertKeys` gets one `InsertKey` call per content key for the rest of the run instead. This mode cannot list all keys in bulk, so a `kv copy` from a json vault relies on the remote server's own duplicate handling.
=== "decrypt_labs mode" === "decrypt_labs mode"
+10 -3
View File
@@ -57,7 +57,12 @@ def process_service_keys(from_vault: Vault, service: str, log: logging.Logger) -
def add_keys_with_progress(vault: Vault, service: str, kid_keys: dict[str, str], log: logging.Logger) -> int: def add_keys_with_progress(vault: Vault, service: str, kid_keys: dict[str, str], log: logging.Logger) -> int:
"""Add content keys to a vault in batches, with a progress bar of counts and time left.""" """Add content keys to a vault in batches, with a progress bar of counts and time left."""
chunk = 1 if type(vault).__name__ == "HTTP" and getattr(vault, "api_mode", None) != "json" else 500
def chunk(i: int) -> int:
if type(vault).__name__ != "HTTP":
return 500
return 500 if i and getattr(vault, "batch_insert", False) else 1
kids = list(kid_keys) kids = list(kid_keys)
added = 0 added = 0
console.print() console.print()
@@ -76,9 +81,11 @@ def add_keys_with_progress(vault: Vault, service: str, kid_keys: dict[str, str],
transient=True, transient=True,
) as progress: ) as progress:
task = progress.add_task(f"{service} → {vault}", total=len(kids), added=0) task = progress.add_task(f"{service} → {vault}", total=len(kids), added=0)
for i in range(0, len(kids), chunk): i = 0
batch: dict[Union[UUID, str], str] = {kid: kid_keys[kid] for kid in kids[i : i + chunk]} while i < len(kids):
batch: dict[Union[UUID, str], str] = {kid: kid_keys[kid] for kid in kids[i : i + chunk(i)]}
added += vault.add_keys(service, batch) added += vault.add_keys(service, batch)
i += len(batch)
progress.update(task, advance=len(batch), added=added) progress.update(task, advance=len(batch), added=added)
return added return added
+8 -3
View File
@@ -93,7 +93,11 @@ class HTTP(Vault):
return {"status": "not_found"} return {"status": "not_found"}
if not r.ok: if not r.ok:
raise ValueError(f"API returned HTTP Error {r.status_code}: {r.reason.title()}") try:
detail = r.json().get("message") or r.reason.title()
except (json.JSONDecodeError, AttributeError):
detail = r.reason.title()
raise ValueError(f"API returned HTTP Error {r.status_code}: {detail}")
try: try:
res = r.json() res = r.json()
@@ -286,9 +290,10 @@ class HTTP(Vault):
) )
if response.get("status") != "not_found": if response.get("status") != "not_found":
return int(response.get("inserted", 0)) return int(response.get("inserted", 0))
# only a 404 proves the server has no InsertKeys method; other
# failures may be transient, so keep batching for the next call
self.batch_insert = False self.batch_insert = False
except ValueError as e:
if "unknown" in str(e).lower() and "method" in str(e).lower():
self.batch_insert = False
except Exception: except Exception:
pass pass
for kid, key in processed_kid_keys.items(): for kid, key in processed_kid_keys.items():