fix: clean error on OS-level write failures in config init and dispatch (v0.1.2)

- config init catches OSError (read-only dir, ENOSPC, gone cwd) alongside CommandError
  and prints a clean [x] line; the main dispatch catches the full OSError family instead
  of only FileNotFoundError (L13)
- document read_flag's fail-closed (non-dict -> not allowed) as a deliberate privilege-
  gate default (nit).

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
disqualifier 2026-06-29 17:58:09 -04:00
parent eced5333d6
commit a40a7432ef
5 changed files with 16 additions and 8 deletions

View File

@ -13,20 +13,20 @@ authorization system and the key-document schema; the crypto primitives live in
## Install ## Install
``` ```
envelope_authorizer @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.1 envelope_authorizer @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.2
``` ```
Direct: Direct:
```bash ```bash
pip install "envelope_authorizer @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.1" pip install "envelope_authorizer @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.2"
``` ```
The base install uses a local JSON file for storage (stdlib only). For shared The base install uses a local JSON file for storage (stdlib only). For shared
dev→server storage, install the mongo extra: dev→server storage, install the mongo extra:
```bash ```bash
pip install "envelope_authorizer[mongo] @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.1" pip install "envelope_authorizer[mongo] @ git+ssh://git@git.rethinkstudios.io/rethink-public/envelope_authorizer.git@v0.1.2"
``` ```
Installing pulls `envelope_crypto` (and `mongo` with the extra). After install, Installing pulls `envelope_crypto` (and `mongo` with the extra). After install,

View File

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "envelope_authorizer" name = "envelope_authorizer"
version = "0.1.1" version = "0.1.2"
description = "CLI key-authorization manager for envelope_crypto" description = "CLI key-authorization manager for envelope_crypto"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [

View File

@ -1 +1 @@
__version__ = "0.1.1" __version__ = "0.1.2"

View File

@ -72,7 +72,9 @@ def main() -> int:
try: try:
config_init.run(None, None, args) config_init.run(None, None, args)
return 0 return 0
except CommandError as error: except (CommandError, OSError) as error:
# config_init writes a file (cwd may be read-only, full, or gone) — an
# OSError must print a clean [✘] line, not a raw traceback
return _fail(str(error)) return _fail(str(error))
parser.parse_args(["config", "--help"]) parser.parse_args(["config", "--help"])
return 0 return 0
@ -92,7 +94,9 @@ def main() -> int:
return 0 return 0
except InvalidTag: except InvalidTag:
return _fail("capability flag failed authentication — tampered or wrong DEK") return _fail("capability flag failed authentication — tampered or wrong DEK")
except (ConfigError, CommandError, RuntimeError, ValueError, FileNotFoundError) as error: except (ConfigError, CommandError, RuntimeError, ValueError, OSError) as error:
# OSError covers the FileNotFoundError/PermissionError/IsADirectoryError family
# so an i/o failure prints a clean [✘] line instead of a traceback
return _fail(str(error)) return _fail(str(error))

View File

@ -22,7 +22,11 @@ def make_flag(crypto: EnvelopeCrypto, allowed: bool) -> dict:
def read_flag(crypto: EnvelopeCrypto, blob: dict) -> bool: def read_flag(crypto: EnvelopeCrypto, blob: dict) -> bool:
"""decrypt a capability flag; return the `allowed` bool""" """decrypt a capability flag; return the `allowed` bool
fails closed: a non-dict / unexpected plaintext reads as not-allowed rather than
raising a privilege gate must default to deny on a malformed flag.
"""
data = crypto.decrypt_data(blob) data = crypto.decrypt_data(blob)
return bool(data.get("allowed", False)) if isinstance(data, dict) else False return bool(data.get("allowed", False)) if isinstance(data, dict) else False