Security: Enforce mandatory API Keys for V3, fix search logic, and update frontend guide
This commit is contained in:
59
documentation/PROJECT_SECURITY_HARDENING.md
Normal file
59
documentation/PROJECT_SECURITY_HARDENING.md
Normal 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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user