fix: a stored key doc with meta:null no longer crashes authorize/revoke
find_by_friendly, authorize.py's existing-target check, and revoke.py's
success-print all did doc.get("meta", {}).get(...) directly - the {} default
only applies when the key is ABSENT, not when its value is null (plausible in
shared mongo or a hand-edited JSON store), so a null meta raised AttributeError.
revoke's crash landed AFTER storage.delete() already succeeded, so a completed
revoke was reported as a raw traceback instead of a clean exit. list_keys._meta
already coerced correctly; that fix is now hoisted into commands/__init__.py as
doc_meta, the single source every meta-reading call site goes through. cli.py's
catch tuple also gains AttributeError as defense in depth for any future
unguarded meta access.
Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
@@ -99,9 +99,12 @@ def main() -> int:
|
||||
return _fail("capability flag failed authentication - tampered or wrong DEK")
|
||||
except PyMongoError as error:
|
||||
return _fail(f"storage backend error: {error}")
|
||||
except (ConfigError, CommandError, RuntimeError, ValueError, OSError, KeyError, TypeError) as error:
|
||||
except (
|
||||
ConfigError, CommandError, RuntimeError, ValueError, OSError, KeyError, TypeError, AttributeError,
|
||||
) as error:
|
||||
# covers a malformed config/doc/flag (incl. KeyError/TypeError from unguarded
|
||||
# indexing) - all print a clean [✘] line, not a raw traceback
|
||||
# indexing, AttributeError from a null field a caller assumed was a dict) -
|
||||
# all print a clean [✘] line, not a raw traceback
|
||||
return _fail(str(error))
|
||||
|
||||
|
||||
|
||||
@@ -68,9 +68,21 @@ def boot_local(config, storage) -> Tuple[EnvelopeCrypto, dict]:
|
||||
return crypto, doc
|
||||
|
||||
|
||||
def doc_meta(doc: dict) -> dict:
|
||||
"""the doc's meta block as a dict, coercing a missing/null/malformed one to {}
|
||||
|
||||
a stored doc's `meta` can be null (shared mongo, a hand-edited JSON file) - the
|
||||
dict.get(key, {}) default only applies when the key is ABSENT, not when its
|
||||
value is null, so every meta-reading call site must go through this rather than
|
||||
`doc.get("meta", {})` directly or it raises AttributeError on a null meta.
|
||||
"""
|
||||
meta = doc.get("meta")
|
||||
return meta if isinstance(meta, dict) else {}
|
||||
|
||||
|
||||
def find_by_friendly(storage, friendly: str) -> Optional[dict]:
|
||||
"""return the doc whose meta.friendly matches, or None"""
|
||||
for doc in storage.get_all():
|
||||
if doc.get("meta", {}).get("friendly") == friendly:
|
||||
if doc_meta(doc).get("friendly") == friendly:
|
||||
return doc
|
||||
return None
|
||||
|
||||
@@ -9,6 +9,7 @@ from . import (
|
||||
CommandError,
|
||||
boot_local,
|
||||
build_doc,
|
||||
doc_meta,
|
||||
find_by_friendly,
|
||||
local_fingerprint,
|
||||
make_flag,
|
||||
@@ -35,7 +36,7 @@ def run(config, storage, args) -> None:
|
||||
)
|
||||
existing = storage.get(new_fp)
|
||||
if existing:
|
||||
existing_friendly = existing.get("meta", {}).get("friendly", "?")
|
||||
existing_friendly = doc_meta(existing).get("friendly", "?")
|
||||
raise CommandError(
|
||||
f"target key is already authorized as '{existing_friendly}'; "
|
||||
f"authorize would silently replace that record - revoke it first "
|
||||
|
||||
@@ -7,7 +7,7 @@ the wrapped key or DEK.
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from . import boot_local, read_flag
|
||||
from . import boot_local, doc_meta, read_flag
|
||||
|
||||
|
||||
def _can_authorize(crypto, doc) -> str:
|
||||
@@ -18,12 +18,6 @@ def _can_authorize(crypto, doc) -> str:
|
||||
return "?"
|
||||
|
||||
|
||||
def _meta(doc) -> dict:
|
||||
"""the doc's meta block as a dict, coercing a missing/null/malformed one to {}"""
|
||||
meta = doc.get("meta")
|
||||
return meta if isinstance(meta, dict) else {}
|
||||
|
||||
|
||||
def _created(meta: dict) -> str:
|
||||
"""format created_at as a UTC timestamp string, or '-' if absent/unparseable"""
|
||||
raw = meta.get("created_at")
|
||||
@@ -44,7 +38,7 @@ def run(config, storage, args) -> None:
|
||||
print(header)
|
||||
print("-" * len(header))
|
||||
for doc in docs:
|
||||
meta = _meta(doc)
|
||||
meta = doc_meta(doc)
|
||||
print(
|
||||
f"{doc.get('_id', '')[:16]:<18} "
|
||||
f"{str(meta.get('friendly', '-')):<16} "
|
||||
|
||||
@@ -7,7 +7,7 @@ rotate the DEK or scrub it from a machine that already unwrapped it.
|
||||
|
||||
from envelope_crypto import EnvelopeCrypto
|
||||
|
||||
from . import CommandError, find_by_friendly, local_fingerprint
|
||||
from . import CommandError, doc_meta, find_by_friendly, local_fingerprint
|
||||
|
||||
_WARNING = (
|
||||
"[!] Revoke removes the record only. It does NOT rotate the DEK or scrub it\n"
|
||||
@@ -47,6 +47,6 @@ def run(config, storage, args) -> None:
|
||||
raise CommandError("refusing to revoke the local key")
|
||||
|
||||
storage.delete(doc["_id"])
|
||||
friendly = doc.get("meta", {}).get("friendly", "?")
|
||||
friendly = doc_meta(doc).get("friendly", "?")
|
||||
print(f"[✔] Revoked: {friendly} ({doc['_id'][:16]})")
|
||||
print(_WARNING)
|
||||
|
||||
Reference in New Issue
Block a user