Files
envelope_authorizer/src/envelope_authorizer/cli.py
T
dsql e5c3547c51 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>
2026-07-06 00:16:52 -04:00

113 lines
4.0 KiB
Python

"""argparse setup and dispatch for the authorizer CLI
`config init` runs without an existing config (it writes one); every other
command loads config and resolves a storage backend first. expected failures
print a clean `[✘] ...` line and exit non-zero - no traceback.
"""
import argparse
import sys
from cryptography.exceptions import InvalidTag
from . import __version__
from .config import ConfigError, load_config
from .commands import CommandError, authorize, config_init, init, list_keys, revoke, verify
from .storage import resolve
try:
from pymongo.errors import PyMongoError
except ImportError:
class PyMongoError(Exception):
"""stand-in when the [mongo] extra is not installed; never actually raised"""
def _build_parser() -> argparse.ArgumentParser:
"""construct the argument parser with all subcommands"""
parser = argparse.ArgumentParser(
prog="authorizer",
description="key-authorization manager for envelope_crypto",
)
parser.add_argument("--version", action="version", version=f"envelope_authorizer {__version__}")
sub = parser.add_subparsers(dest="cmd")
config_cmd = sub.add_parser("config", help="manage the authorizer config file")
config_sub = config_cmd.add_subparsers(dest="config_cmd")
config_sub.add_parser("init", help="write a starter .authorizer.toml in the current directory")
init_cmd = sub.add_parser("init", help="initialize the key system on this machine")
init_cmd.add_argument("--friendly", required=True, help="human name for this key")
auth_cmd = sub.add_parser("authorize", help="authorize another machine's public key")
auth_cmd.add_argument("--key", required=True, help="path to the public key to authorize")
auth_cmd.add_argument("--friendly", required=True, help="human name for the new key")
auth_cmd.add_argument(
"--can-authorize", action="store_true", dest="can_authorize",
help="let the new key authorize others (omit for servers)",
)
sub.add_parser("verify", help="health-check the local crypto setup")
sub.add_parser("list", help="list all authorized keys")
revoke_cmd = sub.add_parser("revoke", help="remove a key's authorization record")
group = revoke_cmd.add_mutually_exclusive_group(required=True)
group.add_argument("--friendly", help="friendly name of the key to revoke")
group.add_argument("--fingerprint", help="fingerprint prefix of the key to revoke")
return parser
def _fail(message: str) -> int:
"""print a clean error line and return a non-zero exit code"""
print(f"[✘] {message}")
return 1
def main() -> int:
"""parse args, dispatch the command, and translate errors to exit codes"""
parser = _build_parser()
args = parser.parse_args()
if args.cmd is None:
parser.print_help()
return 0
if args.cmd == "config":
if args.config_cmd == "init":
try:
config_init.run(None, None, args)
return 0
except (CommandError, OSError) as error:
return _fail(str(error))
parser.parse_args(["config", "--help"])
return 0
handlers = {
"init": init.run,
"authorize": authorize.run,
"verify": verify.run,
"list": list_keys.run,
"revoke": revoke.run,
}
try:
config = load_config()
storage = resolve(config)
handlers[args.cmd](config, storage, args)
return 0
except InvalidTag:
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, AttributeError,
) as error:
# covers a malformed config/doc/flag (incl. KeyError/TypeError from unguarded
# 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))
if __name__ == "__main__":
sys.exit(main())