206 lines
16 KiB
Markdown
206 lines
16 KiB
Markdown
## 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 in `tenant/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.md` to 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)
|
||
1) Subscription / Payment model mismatch (High)
|
||
- Evidence:
|
||
- `backend/igny8_core/auth/models.py::Subscription` defines `stripe_subscription_id = models.CharField(..., unique=True)` and links subscription to account. There is no `payment_method` or `external_payment_id` field.
|
||
- 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_method` enum/choices to `Subscription` and `Account` (values: `stripe`, `paypal`, `bank_transfer`), default `stripe` for backwards compatibility.
|
||
- Add `external_payment_id` (nullable) to record external payment reference (e.g., bank transfer reference number / PayPal transaction id).
|
||
- Make `stripe_subscription_id` nullable and remove unique constraint if you must support multiple non-Stripe flows. Migration steps:
|
||
1. Add new nullable fields and keep old fields unchanged.
|
||
2. Backfill `payment_method='stripe'` where `stripe_subscription_id` exists.
|
||
3. Make `stripe_subscription_id` nullable and drop unique index if needed.
|
||
- Update serializers, admin forms, and registration/subscription endpoints to accept `payment_method`.
|
||
- Files: `backend/igny8_core/auth/models.py`, `backend/igny8_core/api/serializers.py` (where Subscription serializer exists), frontend forms.
|
||
|
||
2) API-key auth bypasses plan/status validation (High)
|
||
- Evidence:
|
||
- `backend/igny8_core/api/authentication.py::APIKeyAuthentication` sets `request.account` and `request.site` but does not call `_validate_account_and_plan` from 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 resolving `account`, call a shared validation helper (e.g., factor `_validate_account_and_plan` into `auth/utils.py` and call it here). If validation fails, raise `AuthenticationFailed` or return `None`.
|
||
- 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.
|
||
|
||
3) 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.
|
||
- Recommended action:
|
||
- Decide: keep `system accounts` with tightly-scoped features (e.g., access to global monitoring endpoints only) OR remove hard-coded system slugs and manage via a `features` or `is_system_account` boolean that must be explicitly set.
|
||
- Files to update/document: `backend/igny8_core/auth/models.py`, `master-docs/00-system/*`.
|
||
|
||
4) Throttle bypass broadness (Medium)
|
||
- Evidence:
|
||
- `backend/igny8_core/api/throttles.py::DebugScopedRateThrottle` currently 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 account` or 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.
|
||
|
||
5) Account injection & validation behavior (Medium)
|
||
- Evidence:
|
||
- `backend/igny8_core/auth/middleware.py::AccountContextMiddleware` skips middleware for `request.path.startswith('/api/v1/auth/')` and `/admin/` and returns immediately in some JWT failure cases (silently allowing `request.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.account` is present for tenant endpoints.
|
||
- Files: `backend/igny8_core/auth/middleware.py`.
|
||
|
||
6) Credit deduction consistent but missing pre-check hooks (Low)
|
||
- Evidence:
|
||
- `backend/igny8_core/ai/engine.py` automatically deducts credits via `CreditService.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()` or `AICore` calls `CreditService.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`.
|
||
|
||
7) Registration & initial credits (Medium)
|
||
- Evidence:
|
||
- `Final_Flow_Tenancy.md` describes initial credits seeded from `Plan.get_effective_credits_per_month()` on registration.
|
||
- Implementation: `auth/views.py::register` (inspected earlier) sets `Account.credits` on 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.
|
||
|
||
8) 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 `admin` endpoint to confirm bank transfer (secure endpoint, permissioned) which:
|
||
- Accepts account_id/subscription_id, `external_payment_id`, payer details, proof (file or URL), amount, and sets `Subscription.status='active'` and updates `Account.credits`.
|
||
- Add `payment_proofs` model or attach to `CreditTransaction`.
|
||
- Files: new `backend/igny8_core/billing/views.py`, updates to `backend/igny8_core/auth/models.py` (Subscription), `backend/igny8_core/business/billing/services/`.
|
||
|
||
9) 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 when `request.account` missing.
|
||
- `perform_create()` raises PermissionDenied when account context missing for account-scoped models.
|
||
- `APIKeyAuthentication` denies access when account is suspended or plan inactive.
|
||
- `DebugScopedRateThrottle` enforces 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.
|
||
|
||
Concrete code and DB changes (step-by-step)
|
||
1) Add payment fields migration (priority: HIGH)
|
||
- Migration pseudo-steps:
|
||
- Add `payment_method = models.CharField(max_length=30, choices=PAYMENT_CHOICES, default='stripe')` to `Subscription` and `Account` (nullable initially if you prefer).
|
||
- Add `external_payment_id = models.CharField(max_length=255, null=True, blank=True)`.
|
||
- Make `stripe_subscription_id` nullable and drop unique constraint if necessary.
|
||
- Update serializers and forms to accept `payment_method`.
|
||
- Update `register` flow and any subscription creation flows to accept `payment_method`.
|
||
|
||
2) API-key validation (priority: HIGH)
|
||
- Implementation:
|
||
- Extract `_validate_account_and_plan()` from `auth/middleware.py` into `auth/utils.py` as `validate_account_and_plan(account_or_user) -> None | (error, status)`.
|
||
- Call that helper from `APIKeyAuthentication.authenticate()` immediately after obtaining `account` and before returning `(user, api_key)`. On failure, raise `AuthenticationFailed` with the same message & HTTP semantics as middleware uses (map 402/403 to DRF errors).
|
||
- Tests:
|
||
- Add tests covering API key access for `active` vs `past_due` / `canceled`.
|
||
|
||
3) Throttle policy update (priority: MEDIUM)
|
||
- Implementation:
|
||
- Remove blanket authenticated bypass. Instead:
|
||
- Keep `debug_bypass` and env variable bypass.
|
||
- Add `if is_system_account_user() or is_developer()` optionally for debug environments only.
|
||
- Add per-account throttle key using `account.id` or `request.user.account.id`.
|
||
- Files: `backend/igny8_core/api/throttles.py`.
|
||
|
||
4) 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`.
|
||
|
||
5) Bank-transfer admin confirmation endpoint (priority: MEDIUM)
|
||
- Implementation:
|
||
- Endpoint: `POST /api/v1/billing/confirm-bank-transfer/` (permissioned to `IsAdminOrOwner` or similar admin role).
|
||
- Accepts: `subscription_id | account_id`, `external_payment_id`, `amount`, `payer_name`, `proof_url` or file upload.
|
||
- Actions:
|
||
- Mark `Subscription.status='active'`, set `current_period_start/end`.
|
||
- Set `Subscription.payment_method='bank_transfer'`, store `external_payment_id`.
|
||
- Add `CreditTransaction` entry and top up `Account.credits` to `Plan.get_effective_credits_per_month()` (or configured top-up).
|
||
- 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).
|
||
|
||
6) 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) and `master-docs/00-system/02-MULTITENANCY-MODEL.md` to remove references to admin bypasses and document new account-only model.
|
||
- Update admin UI to:
|
||
- Show `payment_method` on account/subscription pages.
|
||
- Provide "Confirm Bank Transfer" action (with proof upload).
|
||
- Mark Stripe/PayPal UI options as "coming soon" where relevant.
|
||
|
||
Rollout plan (safe incremental)
|
||
1. Create DB migrations (add nullable fields) and deploy with code that tolerates nulls. (no downtime)
|
||
2. Backfill `payment_method='stripe'` for existing subscriptions where `stripe_subscription_id` exists.
|
||
3. Deploy API changes to accept `payment_method` but keep Stripe/PayPal inactive in business logic.
|
||
4. Add admin confirmation endpoint and small UI changes; deploy.
|
||
5. Gradually switch registration/checkout UI to allow bank transfer (server-side honored immediately).
|
||
6. 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_id` fields (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 in `APIKeyAuthentication`.
|
||
- `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` — add `payment_method` + `external_payment_id`, make `stripe_subscription_id` nullable.
|
||
- `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):
|
||
1. Implement DB migration and model/serializer changes for `payment_method` + `external_payment_id` + make `stripe_subscription_id` nullable (create migrations, update code).
|
||
2. Implement API-key plan/status validation (small PR).
|
||
3. Implement bank-transfer confirmation endpoint and basic admin UI API (POST endpoint only).
|
||
4. 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.
|