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>
113 lines
4.0 KiB
Python
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())
|