16 KiB
Tenancy Audit — Detailed Report (Dec 2025)
This is a detailed, actionable audit of the tenancy model, controls, and billing/payment gaps in the codebase as of Dec 2025. It includes precise findings with file references, concrete fixes (code + DB migration), recommended tests, rollout/rollback guidance, and prioritised next steps.
Summary of actions already applied
- Removed role/system bypasses from account/site/sector filtering and permissions (changes under
backend/igny8_core/...and mirrored intenant/backend/igny8_core/...). These changes make tenant isolation strict: queries, creates and updates now require an account context and will fail/return empty if none is present. - Updated
Final_Flow_Tenancy.mdto present Stripe and PayPal as placeholders and Bank Transfer as the active payment path (document-only change).
Methodology
- Code locations scanned (primary):
backend/igny8_core/auth/middleware.py(account injection & plan validation)backend/igny8_core/api/base.py(AccountModelViewSet, SiteSectorModelViewSet)backend/igny8_core/api/permissions.py(HasTenantAccess, role-based perms)backend/igny8_core/api/throttles.py(DebugScopedRateThrottle)backend/igny8_core/auth/models.py(User, Account, Plan, Subscription)backend/igny8_core/api/authentication.py(JWTAuthentication, APIKeyAuthentication)backend/igny8_core/business/billing/services/credit_service.py&backend/igny8_core/ai/engine.py(credits handling)backend/igny8_core/auth/views.py(Site/Sector ViewSets)
- I searched occurrences of
is_admin_or_developer,is_system_account,stripe_subscription_id,APIKeyAuthentication, and rate-throttle bypasses and inspected relevant code paths.
Detailed findings (evidence + impact)
-
Subscription / Payment model mismatch (High)
- Evidence:
backend/igny8_core/auth/models.py::Subscriptiondefinesstripe_subscription_id = models.CharField(..., unique=True)and links subscription to account. There is nopayment_methodorexternal_payment_idfield.
- Impact:
- Can't represent non-Stripe payments (bank transfer, PayPal) without schema changes. Current docs (Bank Transfer active) are inaccurate relative to DB constraints.
- Recommended fix:
- DB migration to:
- Add
payment_methodenum/choices toSubscriptionandAccount(values:stripe,paypal,bank_transfer), defaultstripefor backwards compatibility. - Add
external_payment_id(nullable) to record external payment reference (e.g., bank transfer reference number / PayPal transaction id). - Make
stripe_subscription_idnullable and remove unique constraint if you must support multiple non-Stripe flows. Migration steps:- Add new nullable fields and keep old fields unchanged.
- Backfill
payment_method='stripe'wherestripe_subscription_idexists. - Make
stripe_subscription_idnullable and drop unique index if needed.
- Update serializers, admin forms, and registration/subscription endpoints to accept
payment_method.
- Add
- DB migration to:
- Files:
backend/igny8_core/auth/models.py,backend/igny8_core/api/serializers.py(where Subscription serializer exists), frontend forms.
- Evidence:
-
API-key auth bypasses plan/status validation (High)
- Evidence:
backend/igny8_core/api/authentication.py::APIKeyAuthenticationsetsrequest.accountandrequest.sitebut does not call_validate_account_and_planfrom middleware. The middleware (auth/middleware.py) runs plan validation only for session/JWT flows.
- Impact:
- WordPress bridge/API-key clients can access tenant-scoped endpoints even if the account is suspended or plan inactive.
- Recommended fix:
- In
APIKeyAuthentication.authenticate(), after resolvingaccount, call a shared validation helper (e.g., factor_validate_account_and_planintoauth/utils.pyand call it here). If validation fails, raiseAuthenticationFailedor returnNone. - Files:
backend/igny8_core/api/authentication.py,backend/igny8_core/auth/middleware.py(extract validation helper),backend/igny8_core/auth/utils.py. - Tests: add API-key request tests for suspended/past-due accounts returning 402/403.
- In
- Evidence:
-
Role/system overrides removed but system-account function remains (Design decision needed) (Medium)
- Evidence:
auth/models.py::Account.is_system_account()returns slug in['aws-admin', 'default-account', 'default']. Many code paths previously used admin/dev/system overrides (these were removed).
- Impact:
- Potential residual special-casing exists (e.g.,
is_system_account_user()), and system accounts may need controlled, explicit privileges rather than silent bypasses.
- Potential residual special-casing exists (e.g.,
- Recommended action:
- Decide: keep
system accountswith tightly-scoped features (e.g., access to global monitoring endpoints only) OR remove hard-coded system slugs and manage via afeaturesoris_system_accountboolean that must be explicitly set. - Files to update/document:
backend/igny8_core/auth/models.py,master-docs/00-system/*.
- Decide: keep
- Evidence:
-
Throttle bypass broadness (Medium)
- Evidence:
backend/igny8_core/api/throttles.py::DebugScopedRateThrottlecurrently bypasses throttling for any authenticated user (previously intended for admin/dev/system). This was simplified but remains permissive.
- Impact:
- Authenticated tenants are effectively unthrottled; DoS or excessive usage by a tenant is possible without tenant-specific limits.
- Recommended fix:
- Change to: bypass only when DEBUG or specific env var set OR user is in
system accountor developer role (if you reintroduce that concept intentionally). Otherwise enforce per-tenant/per-scope throttling. - Add tenant-specific rate bucket logic (e.g., throttle keys by
account.id). - Files:
backend/igny8_core/api/throttles.py. - Tests: verify throttling triggers for authenticated tenant requests above threshold.
- Change to: bypass only when DEBUG or specific env var set OR user is in
- Evidence:
-
Account injection & validation behavior (Medium)
- Evidence:
backend/igny8_core/auth/middleware.py::AccountContextMiddlewareskips middleware forrequest.path.startswith('/api/v1/auth/')and/admin/and returns immediately in some JWT failure cases (silently allowingrequest.account = None).
- Impact:
- Some endpoints may not get account validated; middleware may "fail silently" and allowing unauthenticated or un-validated access in edge cases.
- Recommended fix:
- Harden error handling: log token decode errors at WARNING/ERROR, and fail requests explicitly where appropriate. For API-key or JWT failure, ensure any downstream endpoints guard properly (permissions should reject).
- Consider removing silent fallback in production; allow session auth to handle user but ensure
request.accountis present for tenant endpoints. - Files:
backend/igny8_core/auth/middleware.py.
- Evidence:
-
Credit deduction consistent but missing pre-check hooks (Low)
- Evidence:
backend/igny8_core/ai/engine.pyautomatically deducts credits viaCreditService.deduct_credits_for_operation()after AI responses.
- Impact:
- Good centralisation; ensure pre-check (sufficient credits) is always called before AI invocation to avoid wasted external API calls.
- Recommended fix:
- Verify
AIEngine.execute()orAICorecallsCreditService.check_credits_legacy()or similar BEFORE making AI calls. If not, move pre-check earlier and make AI call cancellable if funds insufficient. - Files:
backend/igny8_core/ai/engine.py,backend/igny8_core/business/billing/services/credit_service.py.
- Verify
- Evidence:
-
Registration & initial credits (Medium)
- Evidence:
Final_Flow_Tenancy.mddescribes initial credits seeded fromPlan.get_effective_credits_per_month()on registration.- Implementation:
auth/views.py::register(inspected earlier) setsAccount.creditson creation (confirm serializer/implementation).
- Impact:
- Ensure atomicity: create User + Account + Subscription and seed credits in a transaction to avoid partial creates.
- Recommended fix:
- Wrap registration flow in a DB transaction; move credit seeding into an idempotent service method.
- Files:
backend/igny8_core/auth/views.py, billing service modules.
- Evidence:
-
Billing webhook architecture (Medium)
- Evidence:
- Billing flows refer largely to Stripe webhooks. Bank transfer is manual in doc; no webhook or admin confirmation API exists.
- Impact:
- Manual processes can lead to inconsistent states (account remains suspended when payment received).
- Recommended changes:
- Add
adminendpoint to confirm bank transfer (secure endpoint, permissioned) which:- Accepts account_id/subscription_id,
external_payment_id, payer details, proof (file or URL), amount, and setsSubscription.status='active'and updatesAccount.credits.
- Accepts account_id/subscription_id,
- Add
payment_proofsmodel or attach toCreditTransaction. - Files: new
backend/igny8_core/billing/views.py, updates tobackend/igny8_core/auth/models.py(Subscription),backend/igny8_core/business/billing/services/.
- Add
- Evidence:
-
Tests & CI regression gaps (High)
- Evidence:
- No test coverage found for strict tenant filtering removal. No tests for API-key gate or bank-transfer flows.
- Recommended test matrix:
- Unit tests:
AccountModelViewSet.get_queryset()filters by request.account; returns none whenrequest.accountmissing.perform_create()raises PermissionDenied when account context missing for account-scoped models.APIKeyAuthenticationdenies access when account is suspended or plan inactive.DebugScopedRateThrottleenforces throttle when expected.
- Integration tests:
- Registration -> account created with credits seeded.
- Bank transfer admin confirmation endpoint flow (once implemented).
- Add to CI pipeline; fail on regressions.
- Unit tests:
- Evidence:
Concrete code and DB changes (step-by-step)
-
Add payment fields migration (priority: HIGH)
- Migration pseudo-steps:
- Add
payment_method = models.CharField(max_length=30, choices=PAYMENT_CHOICES, default='stripe')toSubscriptionandAccount(nullable initially if you prefer). - Add
external_payment_id = models.CharField(max_length=255, null=True, blank=True). - Make
stripe_subscription_idnullable and drop unique constraint if necessary.
- Add
- Update serializers and forms to accept
payment_method. - Update
registerflow and any subscription creation flows to acceptpayment_method.
- Migration pseudo-steps:
-
API-key validation (priority: HIGH)
- Implementation:
- Extract
_validate_account_and_plan()fromauth/middleware.pyintoauth/utils.pyasvalidate_account_and_plan(account_or_user) -> None | (error, status). - Call that helper from
APIKeyAuthentication.authenticate()immediately after obtainingaccountand before returning(user, api_key). On failure, raiseAuthenticationFailedwith the same message & HTTP semantics as middleware uses (map 402/403 to DRF errors).
- Extract
- Tests:
- Add tests covering API key access for
activevspast_due/canceled.
- Add tests covering API key access for
- Implementation:
-
Throttle policy update (priority: MEDIUM)
- Implementation:
- Remove blanket authenticated bypass. Instead:
- Keep
debug_bypassand env variable bypass. - Add
if is_system_account_user() or is_developer()optionally for debug environments only. - Add per-account throttle key using
account.idorrequest.user.account.id.
- Keep
- Files:
backend/igny8_core/api/throttles.py.
- Remove blanket authenticated bypass. Instead:
- Implementation:
-
Harden middleware & logging (priority: MEDIUM)
- Implementation:
- Ensure token decode errors and other exceptions are logged. Fail louder in production (configurable).
- For JWT absent but APIKey present, ensure account validation runs or permission classes enforce it.
- Files:
backend/igny8_core/auth/middleware.py.
- Implementation:
-
Bank-transfer admin confirmation endpoint (priority: MEDIUM)
- Implementation:
- Endpoint:
POST /api/v1/billing/confirm-bank-transfer/(permissioned toIsAdminOrOwneror similar admin role). - Accepts:
subscription_id | account_id,external_payment_id,amount,payer_name,proof_urlor file upload. - Actions:
- Mark
Subscription.status='active', setcurrent_period_start/end. - Set
Subscription.payment_method='bank_transfer', storeexternal_payment_id. - Add
CreditTransactionentry and top upAccount.creditstoPlan.get_effective_credits_per_month()(or configured top-up).
- Mark
- Files: new
backend/igny8_core/billing/views.py,backend/igny8_core/business/billing/services/. - Tests: bank transfer happy path; confirm idempotency (double-confirmation should be safe).
- Endpoint:
- Implementation:
-
Tests & CI (priority: HIGH)
- Add unit tests and integration tests described earlier. Run tests locally and include in CI gating.
Documentation & UX changes
- Update
Final_Flow_Tenancy.md(already done) andmaster-docs/00-system/02-MULTITENANCY-MODEL.mdto remove references to admin bypasses and document new account-only model. - Update admin UI to:
- Show
payment_methodon account/subscription pages. - Provide "Confirm Bank Transfer" action (with proof upload).
- Mark Stripe/PayPal UI options as "coming soon" where relevant.
- Show
Rollout plan (safe incremental)
- Create DB migrations (add nullable fields) and deploy with code that tolerates nulls. (no downtime)
- Backfill
payment_method='stripe'for existing subscriptions wherestripe_subscription_idexists. - Deploy API changes to accept
payment_methodbut keep Stripe/PayPal inactive in business logic. - Add admin confirmation endpoint and small UI changes; deploy.
- Gradually switch registration/checkout UI to allow bank transfer (server-side honored immediately).
- Add tests and enable stricter logging / monitoring.
Rollback guidance
- Revert deployments in order: UI -> backend code -> DB (reverse migration only if safe). Keep feature flags for enabling bank-transfer flows to toggle behavior without DB rollbacks.
Estimated effort & priorities
- Immediate (1–3 days): Add
payment_method,external_payment_idfields (migration + serializer updates), API-key validation change, unit tests for API-key gating. - Short (3–7 days): Implement bank-transfer confirmation endpoint, admin UI changes, and throttling policy adjustments + tests.
- Medium (1–3 weeks): Full rollout across frontend, integration with Stripe/PayPal placeholders, heavier monitoring, and end-to-end tests.
Appendix — Exact code touchpoints (recommendations)
backend/igny8_core/auth/middleware.py— extract validation helper; improve logging.backend/igny8_core/api/authentication.py— add validation call inAPIKeyAuthentication.backend/igny8_core/api/base.py— already updated to require account; verify all viewsets inherit this correctly.backend/igny8_core/api/permissions.py— reviewed and cleaned; add tests.backend/igny8_core/api/throttles.py— tighten bypass rules; add tenant-keyed throttling.backend/igny8_core/auth/models.py— addpayment_method+external_payment_id, makestripe_subscription_idnullable.backend/igny8_core/business/billing/services/credit_service.py— ensure pre-checks before AI calls.- New:
backend/igny8_core/billing/views.py&backend/igny8_core/billing/serializers.py— bank transfer admin confirmation endpoint.
Next steps I can take (pick one and I'll implement):
- Implement DB migration and model/serializer changes for
payment_method+external_payment_id+ makestripe_subscription_idnullable (create migrations, update code). - Implement API-key plan/status validation (small PR).
- Implement bank-transfer confirmation endpoint and basic admin UI API (POST endpoint only).
- Produce test cases and a tests PR skeleton (unittests + integration outlines).
If you'd like, I can start with implementing the DB migration and model changes (#1) and update serializers and the admin list/detail views, then add corresponding tests.