feat: tenancy — products + API keys
Product is the only tenant table this service owns (porte table: organization_id -> product_id, everything below it -- tenant_ref/ branch_ref -- stays an opaque string owned by the consuming product, never a row here). Bcrypt-hashed API keys (passlib, same CryptContext shape as the auto's auth.service), generated once by scripts/create_product.py and never persisted in the clear. require_product (X-Api-Key -> Product, 401 on missing/wrong/soft-deleted) is the porte adaptation of the auto's require_permission — every /v1/* route will depend on it instead of a JWT bearer token. Also ports shared/base_model.py and shared/errors.py verbatim (Global Constraints: soft delete mixins, structured 409 bodies). products migration + real "alembic upgrade head" round-trip test (tests/migrations/, new shared _helpers.py instead of the auto's ad hoc cross-file _run_psql reuse). 13 tests green via `make k8s-test`.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
"""CLI: provisions a new `Product` (a SowAI app consuming this service --
|
||||
auto, crm, ...) and prints its PLAINTEXT API key exactly ONCE. Only the
|
||||
bcrypt hash of the key is ever persisted (`tenancy.models.Product`'s
|
||||
docstring) -- if the printed key is lost, the only recovery is provisioning
|
||||
a new Product (or, once key rotation exists, rotating it; not built yet).
|
||||
|
||||
There is no HTTP endpoint for this on purpose (design spec, decision #5:
|
||||
API keys are per-product and provisioning one is an operator action, not
|
||||
self-service) -- this script is the ONLY writer of `products`.
|
||||
|
||||
Run against the target DB via the app's own session config (DATABASE_URL
|
||||
env, same convention as `auto/backend/scripts/seed_dev_admin.py`):
|
||||
|
||||
uv run python scripts/create_product.py --name auto
|
||||
"""
|
||||
import argparse
|
||||
import asyncio
|
||||
import secrets
|
||||
|
||||
from fiscal_svc.core.db import async_session_maker
|
||||
from fiscal_svc.tenancy.service import create_product
|
||||
|
||||
|
||||
async def _main(name: str) -> None:
|
||||
# `secrets.token_urlsafe` (CSPRNG, not `random`) -- this string IS the
|
||||
# credential, same rigor as any password/token generated in this
|
||||
# codebase. 32 bytes of entropy before urlsafe-base64 encoding.
|
||||
api_key = secrets.token_urlsafe(32)
|
||||
async with async_session_maker() as session:
|
||||
product = await create_product(session, name=name, api_key=api_key)
|
||||
|
||||
print(f"Product criado: {product.name} (id={product.id})")
|
||||
print()
|
||||
print("API key -- copie agora. Só o hash bcrypt fica no banco; esta é a")
|
||||
print("ÚNICA vez que o valor em claro é exibido:")
|
||||
print()
|
||||
print(api_key)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Provisiona um novo Product (consumidor da API do sowai-fiscal-svc)."
|
||||
)
|
||||
parser.add_argument("--name", required=True, help="Nome do produto SowAI (ex.: 'auto').")
|
||||
args = parser.parse_args()
|
||||
asyncio.run(_main(args.name))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user