Files
rar-autopass/tools/apikey.py
Claude Agent c17c1aa4f4 feat(securitate-CORE): redactare creds + auth API-key per cont
Redactare:
- handler RequestValidationError dropeaza input/ctx din 422 (vectorul de
  scurgere a rar_credentials.password pe /v1/prezentari); pastreaza type/loc/msg
- app/security.py: scrub/scrub_text + CredentialRedactingFilter pe root+uvicorn
- models.py: password cu repr=False

Auth API-key:
- app/auth.py: hash SHA-256 in api_keys (cheia in clar emisa o singura data),
  header X-API-Key / Authorization: Bearer, dependency resolve_account_id
- enforcement pe flag AUTOPASS_require_api_key (prod on->401, dev off->cont
  default id=1; cheie prezenta invalida->401 mereu)
- account_id real curge din cheie in ingestie + mapare
- tools/apikey.py: CLI create/rotate/revoke/list (fara endpoint HTTP admin)

16 teste noi (tests/test_security.py). 85 pass total.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 20:02:07 +00:00

92 lines
3.2 KiB
Python

#!/usr/bin/env python3
"""CLI lifecycle chei API (CORE securitate).
Emitere/rotire/revocare/listare chei per cont. Adminul ruleaza pe masina gateway
— nicio suprafata HTTP de admin. Cheia in clar se afiseaza O SINGURA DATA la
creare/rotire; in DB traieste doar hash-ul SHA-256.
Utilizare:
python -m tools.apikey create --account 1
python -m tools.apikey rotate --account 1
python -m tools.apikey revoke --key-id 3
python -m tools.apikey list [--account 1]
"""
from __future__ import annotations
import argparse
import sys
from app.auth import create_api_key, list_keys, revoke_api_key, rotate_api_key
from app.db import get_connection, init_db
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Lifecycle chei API gateway RAR AUTOPASS")
sub = parser.add_subparsers(dest="cmd", required=True)
p_create = sub.add_parser("create", help="emite o cheie noua pentru cont")
p_create.add_argument("--account", type=int, required=True, help="account_id")
p_rotate = sub.add_parser("rotate", help="revoca cheile active ale contului + emite una noua")
p_rotate.add_argument("--account", type=int, required=True, help="account_id")
p_revoke = sub.add_parser("revoke", help="revoca o cheie dupa id")
p_revoke.add_argument("--key-id", type=int, required=True, help="api_keys.id")
p_list = sub.add_parser("list", help="listeaza chei (fara hash)")
p_list.add_argument("--account", type=int, default=None, help="filtreaza pe cont")
args = parser.parse_args(argv)
init_db() # asigura schema (api_keys) + cont default
conn = get_connection()
try:
if args.cmd == "create":
try:
key = create_api_key(conn, args.account)
except ValueError as exc:
print(f"eroare: {exc}", file=sys.stderr)
return 2
print(f"Cheie creata pentru cont {args.account} (pastreaz-o, nu se mai afiseaza):")
print(key)
return 0
if args.cmd == "rotate":
try:
key = rotate_api_key(conn, args.account)
except ValueError as exc:
print(f"eroare: {exc}", file=sys.stderr)
return 2
print(f"Chei vechi revocate. Cheie noua pentru cont {args.account}:")
print(key)
return 0
if args.cmd == "revoke":
ok = revoke_api_key(conn, args.key_id)
if ok:
print(f"Cheie {args.key_id} revocata.")
return 0
print(f"Cheie {args.key_id} inexistenta sau deja revocata.", file=sys.stderr)
return 1
if args.cmd == "list":
rows = list_keys(conn, args.account)
if not rows:
print("(nicio cheie)")
return 0
print(f"{'id':>4} {'cont':>4} {'activa':>6} {'creata':<20} revocata")
for r in rows:
print(
f"{r['id']:>4} {r['account_id']:>4} {('da' if r['active'] else 'nu'):>6} "
f"{(r['created_at'] or ''):<20} {r['revoked_at'] or ''}"
)
return 0
finally:
conn.close()
return 0
if __name__ == "__main__":
sys.exit(main())