svc-infra 0.1.630__tar.gz → 0.1.632__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of svc-infra might be problematic. Click here for more details.
- {svc_infra-0.1.630 → svc_infra-0.1.632}/PKG-INFO +1 -1
- {svc_infra-0.1.630 → svc_infra-0.1.632}/pyproject.toml +2 -1
- svc_infra-0.1.632/src/svc_infra/api/fastapi/admin/__init__.py +3 -0
- svc_infra-0.1.632/src/svc_infra/api/fastapi/admin/add.py +231 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/billing/jobs.py +14 -2
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cache/__init__.py +4 -0
- svc_infra-0.1.632/src/svc_infra/cache/add.py +158 -0
- svc_infra-0.1.632/src/svc_infra/docs/adr/0011-admin-scope-and-impersonation.md +73 -0
- svc_infra-0.1.632/src/svc_infra/docs/cache.md +76 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/security/permissions.py +1 -0
- svc_infra-0.1.630/src/svc_infra/docs/cache.md +0 -18
- {svc_infra-0.1.630 → svc_infra-0.1.632}/README.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/apf_payments/README.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/apf_payments/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/apf_payments/alembic.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/apf_payments/models.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/apf_payments/provider/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/apf_payments/provider/aiydan.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/apf_payments/provider/base.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/apf_payments/provider/registry.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/apf_payments/provider/stripe.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/apf_payments/schemas.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/apf_payments/service.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/apf_payments/settings.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/apf_payments/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/apf_payments/router.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/apf_payments/setup.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/_cookies.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/add.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/gaurd.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/mfa/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/mfa/models.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/mfa/pre_auth.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/mfa/router.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/mfa/security.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/mfa/utils.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/mfa/verify.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/policy.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/providers.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/routers/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/routers/account.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/routers/apikey_router.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/routers/oauth_router.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/routers/session_router.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/security.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/sender.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/settings.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/auth/state.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/billing/router.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/billing/setup.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/cache/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/cache/add.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/db/http.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/db/nosql/mongo/add.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/db/nosql/mongo/health.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/db/sql/README.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/db/sql/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/db/sql/add.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/db/sql/crud_router.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/db/sql/health.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/db/sql/session.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/db/sql/users.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/dependencies/ratelimit.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/docs/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/docs/add.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/docs/landing.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/docs/scoped.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/dual/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/dual/dualize.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/dual/protected.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/dual/public.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/dual/router.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/dual/utils.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/dx.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/ease.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/http/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/http/concurrency.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/http/conditional.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/http/deprecation.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/middleware/debug.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/middleware/errors/handlers.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/middleware/graceful_shutdown.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/middleware/idempotency.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/middleware/idempotency_store.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/middleware/optimistic_lock.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/middleware/ratelimit.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/middleware/ratelimit_store.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/middleware/request_id.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/middleware/request_size_limit.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/middleware/timeout.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/openapi/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/openapi/apply.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/openapi/conventions.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/openapi/models.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/openapi/mutators.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/openapi/pipeline.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/openapi/responses.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/openapi/security.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/ops/add.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/pagination.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/paths/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/paths/auth.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/paths/generic.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/paths/prefix.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/paths/user.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/setup.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/tenancy/add.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/api/fastapi/tenancy/context.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/app/README.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/app/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/app/env.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/app/logging/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/app/logging/add.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/app/logging/filter.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/app/logging/formats.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/app/root.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/billing/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/billing/async_service.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/billing/models.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/billing/quotas.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/billing/schemas.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/billing/service.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/bundled_docs/README.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/bundled_docs/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/bundled_docs/getting-started.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cache/README.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cache/backend.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cache/decorators.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cache/demo.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cache/keys.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cache/recache.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cache/resources.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cache/tags.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cache/ttl.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cache/utils.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/__main__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/db/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/db/nosql/mongo/README.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/db/sql/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/db/sql/alembic_cmds.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/db/sql/sql_export_cmds.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/docs/docs_cmds.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/dx/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/dx/dx_cmds.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/help.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/jobs/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/jobs/jobs_cmds.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/obs/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/obs/obs_cmds.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/sdk/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/cmds/sdk/sdk_cmds.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/foundation/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/foundation/runner.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/data/add.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/data/backup.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/data/erasure.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/data/fixtures.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/data/retention.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/crud_schema.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/inbox.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/base.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/constants.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/core.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/indexes.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/management.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/mongo/README.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/mongo/client.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/mongo/settings.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/mongo/templates/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/mongo/templates/documents.py.tmpl +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/mongo/templates/resources.py.tmpl +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/mongo/templates/schemas.py.tmpl +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/repository.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/resource.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/scaffold.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/service.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/service_with_hooks.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/types.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/nosql/utils.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/outbox.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/README.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/apikey.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/authref.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/base.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/constants.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/core.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/management.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/repository.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/resource.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/scaffold.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/service.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/service_with_hooks.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/templates/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/templates/models_schemas/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/templates/models_schemas/entity/models.py.tmpl +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/templates/models_schemas/entity/schemas.py.tmpl +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/templates/setup/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/templates/setup/alembic.ini.tmpl +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/templates/setup/env_async.py.tmpl +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/templates/setup/env_sync.py.tmpl +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/templates/setup/script.py.mako.tmpl +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/tenant.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/types.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/uniq.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/uniq_hooks.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/utils.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/sql/versioning.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/db/utils.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/acceptance-matrix.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/acceptance.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/adr/0002-background-jobs-and-scheduling.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/adr/0003-webhooks-framework.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/adr/0004-tenancy-model.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/adr/0005-data-lifecycle.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/adr/0006-ops-slos-and-metrics.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/adr/0007-docs-and-sdks.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/adr/0008-billing-primitives.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/adr/0009-acceptance-harness.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/adr/0010-timeouts-and-resource-limits.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/api.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/auth.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/billing.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/cli.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/contributing.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/data-lifecycle.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/database.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/docs-and-sdks.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/environment.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/getting-started.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/idempotency.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/jobs.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/observability.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/ops.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/rate-limiting.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/repo-review.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/security.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/tenancy.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/timeouts-and-resource-limits.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/docs/webhooks.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/dx/add.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/dx/changelog.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/dx/checks.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/http/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/http/client.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/jobs/builtins/outbox_processor.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/jobs/builtins/webhook_delivery.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/jobs/easy.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/jobs/loader.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/jobs/queue.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/jobs/redis_queue.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/jobs/runner.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/jobs/scheduler.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/jobs/worker.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/mcp/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/README.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/add.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/cloud_dash.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/grafana/dashboards/http-overview.json +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/metrics/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/metrics/asgi.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/metrics/base.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/metrics/http.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/metrics/sqlalchemy.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/metrics.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/compose_cloud/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/compose_cloud/templates/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/compose_cloud/templates/agent.yaml.tmpl +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/compose_cloud/templates/docker-compose.cloud.yml.tmpl +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/grafana/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/grafana/dashboards/00_overview.json +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/grafana/dashboards/10_http.json +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/grafana/dashboards/20_db.json +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/grafana/dashboards/30_runtime.json +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/grafana/dashboards/40_clients.json +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/grafana/dashboards/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/grafana/templates/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/grafana/templates/docker-compose.yml.tmpl +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/grafana/templates/prometheus.yml.tmpl +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/grafana/templates/provisioning/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/grafana/templates/provisioning/dashboards.yml +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/providers/grafana/templates/provisioning/datasource.yml +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/settings.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/templates/grafana_dashboard.json +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/templates/prometheus_rules.yml +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/templates/sidecars/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/templates/sidecars/compose/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/templates/sidecars/compose/agent.yaml +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/templates/sidecars/compose/docker-compose.yml +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/templates/sidecars/fly/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/templates/sidecars/fly/agent.yaml +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/templates/sidecars/fly/fly.toml.fragment +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/templates/sidecars/k8s/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/templates/sidecars/k8s/configmap.yaml +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/templates/sidecars/k8s/deployment.yaml +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/templates/sidecars/railway/Dockerfile +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/templates/sidecars/railway/README.md +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/templates/sidecars/railway/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/obs/templates/sidecars/railway/agent.yaml +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/py.typed +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/security/add.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/security/audit.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/security/audit_service.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/security/headers.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/security/hibp.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/security/jwt_rotation.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/security/lockout.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/security/models.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/security/org_invites.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/security/passwords.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/security/session.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/security/signed_cookies.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/utils.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/webhooks/__init__.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/webhooks/add.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/webhooks/fastapi.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/webhooks/router.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/webhooks/service.py +0 -0
- {svc_infra-0.1.630 → svc_infra-0.1.632}/src/svc_infra/webhooks/signing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "svc-infra"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.632"
|
|
4
4
|
description = "Infrastructure for building and deploying prod-ready services"
|
|
5
5
|
authors = ["Ali Khatami <aliikhatami94@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -123,6 +123,7 @@ python_classes = ["Test*",]
|
|
|
123
123
|
python_functions = ["test_*"]
|
|
124
124
|
markers = [
|
|
125
125
|
"acceptance: End-to-end acceptance tests running against the acceptance app or BASE_URL",
|
|
126
|
+
"unit: Unit tests",
|
|
126
127
|
"security: Security and auth hardening tests",
|
|
127
128
|
"ratelimit: Rate limiting and abuse protection tests",
|
|
128
129
|
"concurrency: Idempotency and concurrency control tests",
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import hmac
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
from hashlib import sha256
|
|
11
|
+
from types import SimpleNamespace
|
|
12
|
+
from typing import Any, Callable, Optional
|
|
13
|
+
|
|
14
|
+
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
|
15
|
+
|
|
16
|
+
from ....app.env import get_current_environment
|
|
17
|
+
from ....security.permissions import RequirePermission
|
|
18
|
+
from ..auth.security import Identity, Principal, _current_principal
|
|
19
|
+
from ..auth.state import get_auth_state
|
|
20
|
+
from ..db.sql.session import SqlSessionDep
|
|
21
|
+
from ..dual.protected import roles_router
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _b64u(data: bytes) -> str:
|
|
27
|
+
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _b64u_decode(s: str) -> bytes:
|
|
31
|
+
pad = "=" * ((4 - len(s) % 4) % 4)
|
|
32
|
+
return base64.urlsafe_b64decode(s + pad)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _sign(payload: dict, *, secret: str) -> str:
|
|
36
|
+
body = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
|
37
|
+
sig = hmac.new(secret.encode("utf-8"), body, sha256).digest()
|
|
38
|
+
return _b64u(body) + "." + _b64u(sig)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _verify(token: str, *, secret: str) -> dict:
|
|
42
|
+
try:
|
|
43
|
+
b64_body, b64_sig = token.split(".", 1)
|
|
44
|
+
body = _b64u_decode(b64_body)
|
|
45
|
+
exp_sig = _b64u_decode(b64_sig)
|
|
46
|
+
got_sig = hmac.new(secret.encode("utf-8"), body, sha256).digest()
|
|
47
|
+
if not hmac.compare_digest(exp_sig, got_sig):
|
|
48
|
+
raise ValueError("bad_signature")
|
|
49
|
+
payload = json.loads(body)
|
|
50
|
+
if int(payload.get("exp", 0)) < int(time.time()):
|
|
51
|
+
raise ValueError("expired")
|
|
52
|
+
return payload
|
|
53
|
+
except Exception as e:
|
|
54
|
+
raise ValueError("invalid_token") from e
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def admin_router(*, dependencies: Optional[list[Any]] = None, **kwargs) -> APIRouter:
|
|
58
|
+
"""Role-gated admin router for coarse access control.
|
|
59
|
+
|
|
60
|
+
Use permission guards inside endpoints for fine-grained control.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
return roles_router("admin", **kwargs)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def add_admin(
|
|
67
|
+
app,
|
|
68
|
+
*,
|
|
69
|
+
base_path: str = "/admin",
|
|
70
|
+
enable_impersonation: bool = True,
|
|
71
|
+
secret: Optional[str] = None,
|
|
72
|
+
ttl_seconds: int = 15 * 60,
|
|
73
|
+
cookie_name: str = "impersonation",
|
|
74
|
+
impersonation_user_getter: Optional[Callable[[Any, str], Any]] = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Wire admin surfaces with sensible defaults.
|
|
77
|
+
|
|
78
|
+
- Mounts an admin router under base_path.
|
|
79
|
+
- Optionally enables impersonation start/stop endpoints guarded by permissions.
|
|
80
|
+
- Registers a dependency override to honor impersonation cookie globally (idempotent).
|
|
81
|
+
|
|
82
|
+
impersonation_user_getter: optional callable (request, user_id) -> user object.
|
|
83
|
+
If omitted, defaults to loading from SQLAlchemy User model returned by get_auth_state().
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
# Idempotency: only mount once per app instance
|
|
87
|
+
if getattr(app.state, "_admin_added", False):
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
env = get_current_environment()
|
|
91
|
+
_secret = (
|
|
92
|
+
secret or os.getenv("ADMIN_IMPERSONATION_SECRET") or os.getenv("APP_SECRET") or "dev-secret"
|
|
93
|
+
)
|
|
94
|
+
_ttl = int(os.getenv("ADMIN_IMPERSONATION_TTL", str(ttl_seconds)))
|
|
95
|
+
_cookie = os.getenv("ADMIN_IMPERSONATION_COOKIE", cookie_name)
|
|
96
|
+
|
|
97
|
+
r = admin_router(prefix=base_path, tags=["admin"]) # role-gated
|
|
98
|
+
|
|
99
|
+
async def _default_user_getter(request: Request, user_id: str, session: SqlSessionDep):
|
|
100
|
+
try:
|
|
101
|
+
UserModel, _, _ = get_auth_state()
|
|
102
|
+
except Exception:
|
|
103
|
+
# Fallback: simple shim if auth state not configured
|
|
104
|
+
return SimpleNamespace(id=user_id)
|
|
105
|
+
obj = await session.get(UserModel, user_id)
|
|
106
|
+
if not obj:
|
|
107
|
+
raise HTTPException(404, "user_not_found")
|
|
108
|
+
return obj
|
|
109
|
+
|
|
110
|
+
user_getter = impersonation_user_getter
|
|
111
|
+
|
|
112
|
+
@r.post(
|
|
113
|
+
"/impersonate/start", status_code=204, dependencies=[RequirePermission("admin.impersonate")]
|
|
114
|
+
)
|
|
115
|
+
async def start_impersonation(
|
|
116
|
+
body: dict, request: Request, response: Response, session: SqlSessionDep, identity: Identity
|
|
117
|
+
):
|
|
118
|
+
target_id = (body or {}).get("user_id")
|
|
119
|
+
reason = (body or {}).get("reason", "")
|
|
120
|
+
if not target_id:
|
|
121
|
+
raise HTTPException(422, "user_id_required")
|
|
122
|
+
# Load target for validation (custom getter or default)
|
|
123
|
+
_res = (
|
|
124
|
+
user_getter(request, target_id)
|
|
125
|
+
if user_getter
|
|
126
|
+
else _default_user_getter(request, target_id, session)
|
|
127
|
+
)
|
|
128
|
+
target = await _res if inspect.isawaitable(_res) else _res
|
|
129
|
+
actor: Principal = identity
|
|
130
|
+
payload = {
|
|
131
|
+
"actor_id": getattr(getattr(actor, "user", None), "id", None),
|
|
132
|
+
"target_id": str(getattr(target, "id", target_id)),
|
|
133
|
+
"iat": int(time.time()),
|
|
134
|
+
"exp": int(time.time()) + _ttl,
|
|
135
|
+
"nonce": _b64u(os.urandom(8)),
|
|
136
|
+
}
|
|
137
|
+
token = _sign(payload, secret=_secret)
|
|
138
|
+
response.set_cookie(
|
|
139
|
+
key=_cookie,
|
|
140
|
+
value=token,
|
|
141
|
+
httponly=True,
|
|
142
|
+
samesite="lax",
|
|
143
|
+
secure=(env in ("prod", "production")),
|
|
144
|
+
path="/",
|
|
145
|
+
max_age=_ttl,
|
|
146
|
+
)
|
|
147
|
+
logger.info(
|
|
148
|
+
"admin.impersonation.started",
|
|
149
|
+
extra={
|
|
150
|
+
"actor_id": payload["actor_id"],
|
|
151
|
+
"target_id": payload["target_id"],
|
|
152
|
+
"reason": reason,
|
|
153
|
+
"expires_in": _ttl,
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
# Re-compose override now to wrap any late overrides set by tests/harness
|
|
157
|
+
try:
|
|
158
|
+
_compose_override()
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
@r.post("/impersonate/stop", status_code=204)
|
|
163
|
+
async def stop_impersonation(response: Response):
|
|
164
|
+
response.delete_cookie(_cookie, path="/")
|
|
165
|
+
logger.info("admin.impersonation.stopped")
|
|
166
|
+
|
|
167
|
+
app.include_router(r)
|
|
168
|
+
|
|
169
|
+
# Dependency override: wrap the base principal to honor impersonation cookie.
|
|
170
|
+
# Compose with any existing override (e.g., acceptance app/test harness) and
|
|
171
|
+
# re-compose at startup to capture late overrides.
|
|
172
|
+
def _compose_override():
|
|
173
|
+
existing = app.dependency_overrides.get(_current_principal)
|
|
174
|
+
if existing and getattr(existing, "_is_admin_impersonation_override", False):
|
|
175
|
+
dep_provider = getattr(existing, "_admin_impersonation_base", _current_principal)
|
|
176
|
+
else:
|
|
177
|
+
dep_provider = existing or _current_principal
|
|
178
|
+
|
|
179
|
+
async def _override_current_principal(
|
|
180
|
+
base: Principal = Depends(dep_provider),
|
|
181
|
+
request: Request = None,
|
|
182
|
+
session: SqlSessionDep = None,
|
|
183
|
+
) -> Principal:
|
|
184
|
+
token = request.cookies.get(_cookie) if request else None
|
|
185
|
+
if not token:
|
|
186
|
+
return base
|
|
187
|
+
try:
|
|
188
|
+
payload = _verify(token, secret=_secret)
|
|
189
|
+
except Exception:
|
|
190
|
+
return base
|
|
191
|
+
# Load target user
|
|
192
|
+
target_id = payload.get("target_id")
|
|
193
|
+
if not target_id:
|
|
194
|
+
return base
|
|
195
|
+
# Preserve actor roles/claims so permissions remain that of the actor
|
|
196
|
+
actor_user = getattr(base, "user", None)
|
|
197
|
+
actor_roles = getattr(actor_user, "roles", []) or []
|
|
198
|
+
_res = (
|
|
199
|
+
user_getter(request, target_id)
|
|
200
|
+
if user_getter
|
|
201
|
+
else _default_user_getter(request, target_id, session)
|
|
202
|
+
)
|
|
203
|
+
target = await _res if inspect.isawaitable(_res) else _res
|
|
204
|
+
# Swap user but keep actor for audit if needed
|
|
205
|
+
setattr(base, "actor", getattr(base, "user", None))
|
|
206
|
+
# If target lacks roles, inherit actor roles to maintain permission checks
|
|
207
|
+
try:
|
|
208
|
+
if not getattr(target, "roles", None):
|
|
209
|
+
setattr(target, "roles", actor_roles)
|
|
210
|
+
except Exception:
|
|
211
|
+
# Best-effort; if target object is immutable, fallback by wrapping
|
|
212
|
+
target = SimpleNamespace(id=getattr(target, "id", target_id), roles=actor_roles)
|
|
213
|
+
base.user = target
|
|
214
|
+
base.via = "impersonated"
|
|
215
|
+
return base
|
|
216
|
+
|
|
217
|
+
app.dependency_overrides[_current_principal] = _override_current_principal
|
|
218
|
+
_override_current_principal._is_admin_impersonation_override = True # type: ignore[attr-defined]
|
|
219
|
+
_override_current_principal._admin_impersonation_base = dep_provider # type: ignore[attr-defined]
|
|
220
|
+
|
|
221
|
+
# Compose now (best-effort) and again on startup to wrap any later overrides
|
|
222
|
+
_compose_override()
|
|
223
|
+
try:
|
|
224
|
+
app.add_event_handler("startup", _compose_override)
|
|
225
|
+
except Exception:
|
|
226
|
+
# Best-effort; if app doesn't support event handlers, we already composed once
|
|
227
|
+
pass
|
|
228
|
+
app.state._admin_added = True
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# no extra helpers
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import inspect
|
|
3
4
|
from datetime import datetime, timezone
|
|
4
5
|
from typing import Any, Awaitable, Callable, Dict, Optional
|
|
5
6
|
|
|
@@ -123,6 +124,17 @@ def make_billing_job_handler(
|
|
|
123
124
|
→ emits topic 'billing.invoice.created'
|
|
124
125
|
"""
|
|
125
126
|
|
|
127
|
+
async def _maybe_commit(session: Any) -> None:
|
|
128
|
+
"""Commit if the session exposes a commit method (await if coroutine).
|
|
129
|
+
|
|
130
|
+
This makes the handler resilient in tests/dev where a dummy session is used.
|
|
131
|
+
"""
|
|
132
|
+
commit = getattr(session, "commit", None)
|
|
133
|
+
if callable(commit):
|
|
134
|
+
result = commit()
|
|
135
|
+
if inspect.isawaitable(result):
|
|
136
|
+
await result
|
|
137
|
+
|
|
126
138
|
async def _handler(job: Job) -> None:
|
|
127
139
|
name = job.name
|
|
128
140
|
data: Dict[str, Any] = job.payload or {}
|
|
@@ -136,7 +148,7 @@ def make_billing_job_handler(
|
|
|
136
148
|
async with session_factory() as session:
|
|
137
149
|
svc = AsyncBillingService(session=session, tenant_id=tenant_id)
|
|
138
150
|
total = await svc.aggregate_daily(metric=metric, day_start=day_start)
|
|
139
|
-
await session
|
|
151
|
+
await _maybe_commit(session)
|
|
140
152
|
webhooks.publish(
|
|
141
153
|
"billing.usage_aggregated",
|
|
142
154
|
{
|
|
@@ -161,7 +173,7 @@ def make_billing_job_handler(
|
|
|
161
173
|
invoice_id = await svc.generate_monthly_invoice(
|
|
162
174
|
period_start=period_start, period_end=period_end, currency=currency
|
|
163
175
|
)
|
|
164
|
-
await session
|
|
176
|
+
await _maybe_commit(session)
|
|
165
177
|
webhooks.publish(
|
|
166
178
|
"billing.invoice.created",
|
|
167
179
|
{
|
|
@@ -5,6 +5,8 @@ This module offers high-level decorators for read/write caching, cache invalidat
|
|
|
5
5
|
and resource-based cache management.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from .add import add_cache
|
|
9
|
+
|
|
8
10
|
# Core decorators - main public API
|
|
9
11
|
from .decorators import cached # alias for cache_read
|
|
10
12
|
from .decorators import mutates # alias for cache_write
|
|
@@ -32,4 +34,6 @@ __all__ = [
|
|
|
32
34
|
# Resource-based caching
|
|
33
35
|
"resource",
|
|
34
36
|
"entity",
|
|
37
|
+
# Easy integration helper
|
|
38
|
+
"add_cache",
|
|
35
39
|
]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Easy integration helper to wire the cache backend into an ASGI app lifecycle.
|
|
5
|
+
|
|
6
|
+
Contract:
|
|
7
|
+
- Idempotent: multiple calls are safe; startup/shutdown handlers are registered once.
|
|
8
|
+
- Env-driven defaults: respects CACHE_URL/REDIS_URL, CACHE_PREFIX, CACHE_VERSION, APP_ENV.
|
|
9
|
+
- Lifecycle: registers startup (init + readiness probe) and shutdown (graceful close).
|
|
10
|
+
- Ergonomics: exposes the underlying cache instance at app.state.cache by default.
|
|
11
|
+
|
|
12
|
+
This does not replace the per-function decorators (`cache_read`, `cache_write`) and
|
|
13
|
+
does not alter existing direct APIs; it simply standardizes initialization and wiring.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
from typing import Any, Callable, Optional
|
|
19
|
+
|
|
20
|
+
from svc_infra.cache.backend import DEFAULT_READINESS_TIMEOUT
|
|
21
|
+
from svc_infra.cache.backend import instance as _instance
|
|
22
|
+
from svc_infra.cache.backend import setup_cache as _setup_cache
|
|
23
|
+
from svc_infra.cache.backend import shutdown_cache as _shutdown_cache
|
|
24
|
+
from svc_infra.cache.backend import wait_ready as _wait_ready
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _derive_settings(
|
|
30
|
+
url: Optional[str], prefix: Optional[str], version: Optional[str]
|
|
31
|
+
) -> tuple[str, str, str]:
|
|
32
|
+
"""Derive cache settings from parameters or environment variables.
|
|
33
|
+
|
|
34
|
+
Precedence:
|
|
35
|
+
- explicit function arguments
|
|
36
|
+
- environment variables (CACHE_URL/REDIS_URL, CACHE_PREFIX, CACHE_VERSION)
|
|
37
|
+
- sensible defaults (mem://, "svc", "v1")
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
derived_url = url or os.getenv("CACHE_URL") or os.getenv("REDIS_URL") or "mem://"
|
|
41
|
+
derived_prefix = prefix or os.getenv("CACHE_PREFIX") or "svc"
|
|
42
|
+
derived_version = version or os.getenv("CACHE_VERSION") or "v1"
|
|
43
|
+
return derived_url, derived_prefix, derived_version
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def add_cache(
|
|
47
|
+
app: Any | None = None,
|
|
48
|
+
*,
|
|
49
|
+
url: str | None = None,
|
|
50
|
+
prefix: str | None = None,
|
|
51
|
+
version: str | None = None,
|
|
52
|
+
readiness_timeout: float | None = None,
|
|
53
|
+
expose_state: bool = True,
|
|
54
|
+
state_key: str = "cache",
|
|
55
|
+
) -> Callable[[], None]:
|
|
56
|
+
"""Wire cache initialization and lifecycle into the ASGI app.
|
|
57
|
+
|
|
58
|
+
If an app is provided, registers startup/shutdown handlers. Otherwise performs
|
|
59
|
+
immediate initialization (best-effort) without awaiting readiness.
|
|
60
|
+
|
|
61
|
+
Returns a no-op shutdown callable for API symmetry with other helpers.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
# Compute effective settings
|
|
65
|
+
eff_url, eff_prefix, eff_version = _derive_settings(url, prefix, version)
|
|
66
|
+
|
|
67
|
+
# If no app provided, do a simple init and return
|
|
68
|
+
if app is None:
|
|
69
|
+
try:
|
|
70
|
+
_setup_cache(url=eff_url, prefix=eff_prefix, version=eff_version)
|
|
71
|
+
logger.info(
|
|
72
|
+
"Cache initialized (no app wiring): backend=%s namespace=%s",
|
|
73
|
+
eff_url,
|
|
74
|
+
f"{eff_prefix}:{eff_version}",
|
|
75
|
+
)
|
|
76
|
+
except Exception:
|
|
77
|
+
logger.exception("Cache initialization failed (no app wiring)")
|
|
78
|
+
return lambda: None
|
|
79
|
+
|
|
80
|
+
# Idempotence: avoid duplicate wiring
|
|
81
|
+
try:
|
|
82
|
+
state = getattr(app, "state", None)
|
|
83
|
+
already = bool(getattr(state, "_svc_cache_wired", False))
|
|
84
|
+
except Exception:
|
|
85
|
+
state = None
|
|
86
|
+
already = False
|
|
87
|
+
|
|
88
|
+
if already:
|
|
89
|
+
logger.debug("add_cache: app already wired; skipping re-registration")
|
|
90
|
+
return lambda: None
|
|
91
|
+
|
|
92
|
+
# Define lifecycle handlers
|
|
93
|
+
async def _startup():
|
|
94
|
+
_setup_cache(url=eff_url, prefix=eff_prefix, version=eff_version)
|
|
95
|
+
try:
|
|
96
|
+
await _wait_ready(timeout=readiness_timeout or DEFAULT_READINESS_TIMEOUT)
|
|
97
|
+
except Exception:
|
|
98
|
+
# Bubble up to fail fast on startup; tests and prod prefer visibility
|
|
99
|
+
logger.exception("Cache readiness probe failed during startup")
|
|
100
|
+
raise
|
|
101
|
+
# Expose cache instance for convenience
|
|
102
|
+
if expose_state and hasattr(app, "state"):
|
|
103
|
+
try:
|
|
104
|
+
setattr(app.state, state_key, _instance())
|
|
105
|
+
except Exception:
|
|
106
|
+
logger.debug("Unable to expose cache instance on app.state", exc_info=True)
|
|
107
|
+
|
|
108
|
+
async def _shutdown():
|
|
109
|
+
try:
|
|
110
|
+
await _shutdown_cache()
|
|
111
|
+
except Exception:
|
|
112
|
+
# Best-effort; shutdown should not crash the app
|
|
113
|
+
logger.debug("Cache shutdown encountered errors (ignored)", exc_info=True)
|
|
114
|
+
|
|
115
|
+
# Register event handlers when supported
|
|
116
|
+
register_ok = False
|
|
117
|
+
try:
|
|
118
|
+
if hasattr(app, "add_event_handler"):
|
|
119
|
+
app.add_event_handler("startup", _startup)
|
|
120
|
+
app.add_event_handler("shutdown", _shutdown)
|
|
121
|
+
register_ok = True
|
|
122
|
+
except Exception:
|
|
123
|
+
register_ok = False
|
|
124
|
+
|
|
125
|
+
if not register_ok:
|
|
126
|
+
# Fallback: attempt FastAPI/Starlette .on_event decorators dynamically
|
|
127
|
+
try:
|
|
128
|
+
on_event = getattr(app, "on_event", None)
|
|
129
|
+
if callable(on_event):
|
|
130
|
+
on_event("startup")(_startup) # type: ignore[misc]
|
|
131
|
+
on_event("shutdown")(_shutdown) # type: ignore[misc]
|
|
132
|
+
register_ok = True
|
|
133
|
+
except Exception:
|
|
134
|
+
register_ok = False
|
|
135
|
+
|
|
136
|
+
# Mark wired and expose state immediately if desired
|
|
137
|
+
if hasattr(app, "state"):
|
|
138
|
+
try:
|
|
139
|
+
setattr(app.state, "_svc_cache_wired", True)
|
|
140
|
+
if expose_state and not hasattr(app.state, state_key):
|
|
141
|
+
setattr(app.state, state_key, _instance())
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
if register_ok:
|
|
146
|
+
logger.info("Cache wired: url=%s namespace=%s", eff_url, f"{eff_prefix}:{eff_version}")
|
|
147
|
+
else:
|
|
148
|
+
# If we cannot register handlers, at least initialize now
|
|
149
|
+
try:
|
|
150
|
+
_setup_cache(url=eff_url, prefix=eff_prefix, version=eff_version)
|
|
151
|
+
except Exception:
|
|
152
|
+
logger.exception("Cache initialization failed (no event registration)")
|
|
153
|
+
|
|
154
|
+
# Return a simple shutdown handle for symmetry with other add_* helpers
|
|
155
|
+
return lambda: None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
__all__ = ["add_cache"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# 0011 — Admin scope, permissions, and impersonation
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
- The codebase already provides RBAC/permission helpers: `RequireRoles`, `RequirePermission`, ABAC via `RequireABAC`/`owns_resource`.
|
|
5
|
+
- The central permission registry maps roles → permissions (`svc_infra.security.permissions.PERMISSION_REGISTRY`). Notably, the `admin` role includes: `user.read`, `user.write`, `billing.read`, `billing.write`, and `security.session.{list,revoke}`.
|
|
6
|
+
- Acceptance tests demonstrate an “admin-only” route guarded by `RequirePermission("user.write")` and temporary role override to `admin`.
|
|
7
|
+
- There is no dedicated admin API surface yet, and no impersonation flow; observability docs mention an optional route classifier that can label routes like `public|internal|admin`.
|
|
8
|
+
|
|
9
|
+
## Goals
|
|
10
|
+
- Define a consistent approach for admin-only surfaces and permission alignment.
|
|
11
|
+
- Establish minimal permissions needed for admin operations, including impersonation.
|
|
12
|
+
- Outline an impersonation flow with security and audit guardrails.
|
|
13
|
+
- Prepare for an easy integration helper (`add_admin`) without implementing it yet.
|
|
14
|
+
|
|
15
|
+
## Non-goals
|
|
16
|
+
- Implement admin endpoints or impersonation logic in this ADR.
|
|
17
|
+
- Replace existing permissions/guards — this ADR aligns and extends them.
|
|
18
|
+
|
|
19
|
+
## Decisions
|
|
20
|
+
|
|
21
|
+
1) Permissions alignment and additions
|
|
22
|
+
- Keep permissions as the canonical guard unit; roles remain a mapping to permissions.
|
|
23
|
+
- Extend the registry with a dedicated permission for impersonation:
|
|
24
|
+
- `admin.impersonate`
|
|
25
|
+
- Keep existing entries (`security.session.{list,revoke}` etc.) as-is.
|
|
26
|
+
- Recommended role → permission mapping updates:
|
|
27
|
+
- `admin`: add `admin.impersonate` (retains existing permissions).
|
|
28
|
+
- `auditor`: keep `audit.read` (already present) and may expand in the future.
|
|
29
|
+
|
|
30
|
+
2) Admin router pattern
|
|
31
|
+
- Provide an admin-only router pattern that layers role and permission checks consistently:
|
|
32
|
+
- Top-level: role gate via `RequireRoles("admin")` to reflect the “admin area”.
|
|
33
|
+
- Endpoint-level: permission gates via `RequirePermission(...)` for specific operations.
|
|
34
|
+
- Rationale: roles communicate the coarse-grained area; fine-grained actions are enforced by permissions.
|
|
35
|
+
- A future helper `admin_router()` can wrap `roles_router("admin")` (from `api.fastapi.dual.protected`) for ergonomic mounting.
|
|
36
|
+
|
|
37
|
+
3) Impersonation flow (design)
|
|
38
|
+
- Endpoints:
|
|
39
|
+
- `POST /admin/impersonate/start` — body: `{ user_id, reason }`; requires `admin.impersonate`.
|
|
40
|
+
- `POST /admin/impersonate/stop` — ends the session.
|
|
41
|
+
- Mechanics:
|
|
42
|
+
- When starting, issue a short-lived, signed impersonation token (or set a dedicated cookie) that encodes: original admin principal id, target user id, issued-at, expires-at, and nonce.
|
|
43
|
+
- Downstream identity resolution should reflect the impersonated user for request handling, while preserving the original admin as the "actor" for auditing.
|
|
44
|
+
- Stopping invalidates the token/cookie (server-side revocation list or versioned secret), and subsequent requests fall back to the admin’s own identity.
|
|
45
|
+
- Safety guardrails:
|
|
46
|
+
- Always require `admin.impersonate`.
|
|
47
|
+
- Enforce explicit `reason` and capture request fingerprint (ip hash, user-agent) with the event.
|
|
48
|
+
- Limit scope by tenant/org if applicable; optionally block actions explicitly marked non-impersonable.
|
|
49
|
+
- Set short TTL (e.g., 15 minutes) with sliding refresh disabled.
|
|
50
|
+
|
|
51
|
+
4) Audit logging
|
|
52
|
+
- Emit structured audit events for impersonation lifecycle:
|
|
53
|
+
- `admin.impersonation.started` with actor, target, reason, ip hash, user-agent, and expiry.
|
|
54
|
+
- `admin.impersonation.stopped` with actor, target, and termination reason (expired/manual).
|
|
55
|
+
- Implementation options (future):
|
|
56
|
+
- Minimal: log via the existing logging setup (structured logger, e.g., `logger.bind(...).info("audit", ...)`).
|
|
57
|
+
- Preferred: emit to an audit outbox/table or webhook channel for retention and cross-system visibility.
|
|
58
|
+
|
|
59
|
+
5) Observability and route classification
|
|
60
|
+
- Encourage passing a `route_classifier` that labels admin routes as `admin` (e.g., for `/admin` base path) so metrics/SLO dashboards can split traffic into `public|internal|admin` classes.
|
|
61
|
+
|
|
62
|
+
## Consequences
|
|
63
|
+
- Clear, documented permissions and flow for admin-only features.
|
|
64
|
+
- Minimal surface to add later: `admin_router()` and `add_admin(app, ...)` helper that mounts admin routes and wires impersonation endpoints + audit hooks.
|
|
65
|
+
- Tests to plan when implementing:
|
|
66
|
+
- Role vs permission gating behavior on /admin routes.
|
|
67
|
+
- Impersonation start/stop lifecycle and audit emission.
|
|
68
|
+
- Ownership checks that permit admin bypass where intended (e.g., session revocation).
|
|
69
|
+
|
|
70
|
+
## Follow-ups
|
|
71
|
+
- Update the permission registry to include `admin.impersonate` (and map into `admin`).
|
|
72
|
+
- Implement `admin_router()` and the `add_admin` helper following this ADR.
|
|
73
|
+
- Add admin acceptance tests and documentation for guardrails and operational guidance.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Cache guide
|
|
2
|
+
|
|
3
|
+
The cache module wraps [cashews](https://github.com/Krukov/cashews) with decorators and namespace helpers so services can centralize key formats.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from svc_infra.cache import cache_read, cache_write, init_cache
|
|
7
|
+
|
|
8
|
+
init_cache() # uses CACHE_PREFIX / CACHE_VERSION
|
|
9
|
+
|
|
10
|
+
@cache_read(key="user:{user_id}", ttl=300)
|
|
11
|
+
async def get_user(user_id: int):
|
|
12
|
+
...
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Environment
|
|
16
|
+
|
|
17
|
+
- `CACHE_PREFIX`, `CACHE_VERSION` – change the namespace alias used by the decorators. 【F:src/svc_infra/cache/README.md†L20-L173】
|
|
18
|
+
- `CACHE_TTL_DEFAULT`, `CACHE_TTL_SHORT`, `CACHE_TTL_LONG` – override canonical TTL buckets. 【F:src/svc_infra/cache/ttl.py†L26-L55】
|
|
19
|
+
|
|
20
|
+
## Easy integration: add_cache
|
|
21
|
+
|
|
22
|
+
Use the one-liner helper to wire cache initialization into your ASGI app lifecycle with sensible defaults. This doesn’t replace the decorators; it standardizes init/readiness/shutdown and exposes a handle for convenience.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from fastapi import FastAPI
|
|
26
|
+
from svc_infra.cache import add_cache, cache_read, cache_write, resource
|
|
27
|
+
|
|
28
|
+
app = FastAPI()
|
|
29
|
+
|
|
30
|
+
# Wires startup (init + readiness) and shutdown (graceful close). Idempotent.
|
|
31
|
+
add_cache(app)
|
|
32
|
+
|
|
33
|
+
user = resource("user", "user_id")
|
|
34
|
+
|
|
35
|
+
@user.cache_read(suffix="profile", ttl=300)
|
|
36
|
+
async def get_user_profile(user_id: int):
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
@user.cache_write()
|
|
40
|
+
async def update_user_profile(user_id: int, payload):
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
# Optional: direct cache instance for advanced scenarios
|
|
44
|
+
# available after startup when using add_cache(app)
|
|
45
|
+
# app.state.cache -> cashews cache instance
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Env-driven defaults
|
|
49
|
+
|
|
50
|
+
- URL: `CACHE_URL` → `REDIS_URL` → `mem://`
|
|
51
|
+
- Prefix: `CACHE_PREFIX` (default `svc`)
|
|
52
|
+
- Version: `CACHE_VERSION` (default `v1`)
|
|
53
|
+
|
|
54
|
+
You can override explicitly:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
add_cache(app, url="redis://localhost:6379/0", prefix="myapp", version="v2")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Behavior
|
|
61
|
+
|
|
62
|
+
- Idempotent: multiple calls won’t duplicate handlers.
|
|
63
|
+
- Startup/shutdown hooks: registered when supported by the app; startup performs a readiness probe. Startup is optional for correctness, but recommended for production reliability.
|
|
64
|
+
- app.state exposure: by default, exposes `app.state.cache` to access the underlying cashews instance.
|
|
65
|
+
|
|
66
|
+
### No-app usage
|
|
67
|
+
|
|
68
|
+
If you’re not wiring an app (e.g., a script), you can initialize without startup hooks:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from svc_infra.cache import add_cache
|
|
72
|
+
|
|
73
|
+
shutdown = add_cache(None) # immediate init (best-effort)
|
|
74
|
+
# ... do work ...
|
|
75
|
+
# call shutdown() is a no-op placeholder for symmetry
|
|
76
|
+
```
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# Cache guide
|
|
2
|
-
|
|
3
|
-
The cache module wraps [cashews](https://github.com/Krukov/cashews) with decorators and namespace helpers so services can centralize key formats.
|
|
4
|
-
|
|
5
|
-
```python
|
|
6
|
-
from svc_infra.cache import cache_read, cache_write, init_cache
|
|
7
|
-
|
|
8
|
-
init_cache() # uses CACHE_PREFIX / CACHE_VERSION
|
|
9
|
-
|
|
10
|
-
@cache_read(key="user:{user_id}", ttl=300)
|
|
11
|
-
async def get_user(user_id: int):
|
|
12
|
-
...
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
### Environment
|
|
16
|
-
|
|
17
|
-
- `CACHE_PREFIX`, `CACHE_VERSION` – change the namespace alias used by the decorators. 【F:src/svc_infra/cache/README.md†L20-L173】
|
|
18
|
-
- `CACHE_TTL_DEFAULT`, `CACHE_TTL_SHORT`, `CACHE_TTL_LONG` – override canonical TTL buckets. 【F:src/svc_infra/cache/ttl.py†L26-L55】
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|