Security: Enforce mandatory API Keys for V3, fix search logic, and update frontend guide

This commit is contained in:
Scott Idem
2026-01-19 14:11:13 -05:00
parent d8b0c3b0a4
commit cad0d2e867
5 changed files with 325 additions and 43 deletions

View File

@@ -0,0 +1,59 @@
# Project: API Security Hardening (V3)
**Status:** Draft / Planning
**Date:** Jan 18, 2026
**Owner:** Scott / Aether API Team
## 1. Executive Summary
This project aims to close a critical security vulnerability in the Aether API V3 dependencies where the `x_no_account_id` header allows unauthorized "Superuser/Bypass" access without validation. Additionally, it addresses the lack of cryptographic verification for JWTs in the V3 CRUD layer.
## 2. The Vulnerabilities
### A. The "Bypass Header" (Critical)
* **Issue:** The `get_account_context_optional` dependency in `app/routers/dependencies_v3.py` accepts a header `x_no_account_id`. If present, it sets `auth_method = 'bypass'` and grants full administrative privileges (`super=True`).
* **Note:** This header may be sent as `x_no_account_id` or `x-no-account-id`. FastAPI's `Header` logic should handle the conversion, but validation must be explicit.
* **Current State:** There is **no** validation of this header. Sending `x_no_account_id: anything` works.
* **Risk:** Total system compromise via simple header injection.
### B. JWT Verification (High)
* **Issue:** The system validates users by looking up the `x_account_id` string in the database (Redis/MariaDB).
* **Current State:** It does **not** cryptographically verify the JWT signature or expiration in the V3 dependency chain.
* **Risk:** Account impersonation if an Account ID is leaked.
## 3. Implementation Plan
### Phase 1: Secure the Bypass Header (Immediate)
**Goal:** Restrict "Bypass Mode" to clients possessing a valid, active API Key.
1. **Refactor Dependencies:**
* Update `get_account_context_optional` to accept `x_aether_api_key`.
* **Logic Change:** If `x_no_account_id` header is detected:
* **Require** `x_aether_api_key`.
* **Lookup** key in `api_key` table.
* **Verify** `enable = 1` and `now()` is between `enable_from` and `enable_to`.
* **Failure Behavior:** If key is invalid/missing, log warning and deny 'bypass' status (fall back to 'guest').
2. **Verification:**
* Test `curl` with header only -> Expect 403/Forbidden.
* Test `curl` with header + valid Key -> Expect 200/OK.
* Test Frontend File Download (uses `x_no_account_id_token` param) -> Expect 200/OK (No regression).
### Phase 2: JWT Verification (Subsequent)
**Goal:** Replace DB-lookup auth with cryptographic JWT validation.
1. **Audit:** Confirm `app/lib_jwt.py` logic is sound.
2. **Middleware/Dependency:** Integrate `decode_jwt` into the `get_account_context` flow.
3. **Transition:** Allow dual-mode (DB lookup OR JWT) for a transition period if necessary, or cut over if frontend sends valid JWTs.
## 4. Impact Analysis
* **Frontend (SvelteKit):**
* Audit confirmed no usage of `x_no_account_id` **header** in active source code.
* Usage of `x_no_account_id_token` (Query Param) is **safe** and distinct from the header logic.
* **Scripts/External Tools:**
* Any external scripts using the "Bypass" header must be updated to send a valid API Key.
## 5. Action Items
- [ ] Create `PROJECT_SECURITY_HARDENING.md` (This document).
- [ ] Refactor `app/routers/dependencies_v3.py`.
- [ ] Verify fix with `curl` tests.
- [ ] Commit changes.

View File

@@ -19,30 +19,38 @@ This guide explains how to update or create frontend functions to interact with
## 2. Authentication and Security (Mandatory in V3)
As of January 2026, the V3 architecture enforces strict **Multi-Tenant Isolation**. Most requests now require valid authentication to prevent data leakage between accounts.
As of January 2026, the V3 architecture enforces strict **Multi-Tenant Isolation** and **Machine Authorization**. Most requests now require two levels of validation.
### A. Authentication Requirement
Almost all V3 CRUD endpoints require a standard Bearer token in the `Authorization` header.
### A. The "Entry Ticket" (API Key)
**Mandatory for all requests.** To prevent unauthorized clients from using the API, every request must provide a valid `x-aether-api-key` in the header. This identifies the application or script (e.g., the Svelte frontend, the Flask legacy app, or an AI agent).
* **Mandatory:** You must provide a valid JWT for nearly all requests.
* **Account Isolation:** The backend automatically filters all results based on the `account_id` found in your JWT. You cannot access data belonging to another account even if you know the random ID.
* **Status Codes:**
* `401 Unauthorized`: Your JWT is invalid or expired.
* `403 Forbidden`: No authentication provided, or you attempted to access an object belonging to a different account.
* **Header:** `x-aether-api-key: <your_app_key>`
* **Status Code:** `403 Forbidden` if missing, invalid, or expired.
**Example Request Header:**
```http
Authorization: Bearer <your_jwt_token>
```
### B. The "Visa" (Account Context)
Once the API Key is validated, you must specify the context of your request.
### B. The "Bootstrap Paradox" Exception (`site_domain`)
1. **Standard User Access**: Provide the `x-account-id` (the random string ID). This restricts all results to that specific tenant.
* **Header:** `x-account-id: <account_id_random>`
2. **Administrative Bypass**: For authorized scripts needing global access, use the bypass header.
* **Header:** `x-no-account-id: bypass`
3. **Authentication Requirement**: Most V3 CRUD endpoints also require a standard Bearer token in the `Authorization` header for user-level actions.
### C. The "Bootstrap Paradox" Exception (`site_domain`)
There is one critical exception to strict authentication: **`site_domain` search**.
Because the frontend needs to lookup the site configuration (to know which account it's on) *before* a user can log in, the following endpoint allows unauthenticated (guest) access:
Because the frontend needs to lookup the site configuration (to know which account it's on) *before* it has an Account ID or User token, the following endpoint allows unauthenticated (**guest**) access:
**Endpoint:** `POST /v3/crud/site_domain/search`
This is the only V3 search allowed without a JWT. All other object types (journal, account, post, etc.) will return `403 Forbidden` if accessed without a token.
This is the only V3 search allowed without an Account ID or JWT. However, it **still requires a valid API Key**.
### D. Fail-Fast on Auth Failures (401/403)
To prevent unnecessary server load and looping, the frontend implements a **Fail-Fast** policy for authentication and authorization errors.
* **401 Unauthorized**: Means the token is missing, invalid, or expired.
* **403 Forbidden**: Means the token is valid, but you do not have permission to access the specific resource (or you attempted a cross-tenant request without bypass).
* **Protocol**: If the API returns a 401 or 403, the frontend **must not retry** the request automatically. It should stop immediately and, if applicable, redirect the user to the login page or display a "Permission Denied" message.
---
@@ -185,6 +193,72 @@ const downloadUrl = `${BASE_URL}/v3/crud/hosted_file/${fileId}/?jwt=${jwtToken}`
## 7. Best Practices for V3
1. **Use `view` for Rich Data**: Instead of manually joining data in separate calls, use `?view=enriched` or `?view=detail`.
2. **Singular Nouns**: Always use singular names for `obj_type` (e.g., `journal`).
3. **Strict Typing**: Ensure your `data` objects match the backend models to avoid `400 Bad Request` validation errors.
---
## 8. Standard Patterns & Common Pitfalls (2026 Update)
To ensure stability across the Aether mesh, all frontend components must adhere to these established patterns.
### A. The "Whitelist" Standard for Saves
**Problem:** Sending the entire object returned by a GET request back to a PATCH endpoint will cause a `400 Bad Request`. This happens because the object contains technical metadata (like `journal_id`, `created_on`, `for_type`) that the API cannot "SET".
**Standard:** When preparing a save payload, explicitly whitelist ONLY the user-editable fields.
```ts
// ✅ CORRECT: Whitelist only editable fields
const data_kv = {
name: tmp_obj.name,
content: tmp_obj.content,
tags: tmp_obj.tags,
category_code: tmp_obj.category_code
};
// ❌ WRONG: Passing the whole object or just blacklisting a few
const data_kv = { ...tmp_obj };
delete data_kv.id; // Still contains other computed columns!
```
### B. The Triple-ID Save Pattern
**Standard:** When sending an ID in a POST/PATCH payload (e.g. creating a child object), use the random string version with the `_id_random` suffix.
* **Property:** `[obj_type]_id_random`
* **Value:** A string (e.g., `qpgvOh5nOYI`)
**Warning:** Sending a string value under an integer key (e.g. `journal_id: "qpgvOh5nOYI"`) will trigger a Pydantic validation error on the backend.
### C. Handle 'NULL' vs '0' for Booleans
**Pitfall:** Fields like `hide` or `priority` can be `NULL` in the database.
**Standard:** Ensure your synchronization logic in components handles `null`, `undefined`, and `0` identically for boolean checks to prevent "ghost" changes being detected by Svelte 5.