Arch: Finalize V3 Auth modularization and Unified Agent spec.

- Integrated zero-dependency Auth models and dependencies_v3.py.
- Successfully resolved circular dependency boot loops.
- Verified site_domain search exception via verify_v3_exceptions.py.
- Refined Unified Agent Architecture with Storage Layer and API-driven access details.
- Updated project roadmap and milestones in GEMINI.md.
This commit is contained in:
Scott Idem
2026-01-07 19:07:21 -05:00
parent 90c6b914fa
commit 59d5b81da0
7 changed files with 245 additions and 307 deletions

View File

@@ -11,55 +11,43 @@ I am the **primary orchestrator and main helper** for the development of the **U
- **Owner/Developer:** Scott Idem (user).
- **System Name:** Aether (AE).
- **Purpose:** Events Presentation Management, Events Badge Printing, Leads, Attendee Tracking, Presentation Launcher, Journals, Archives, Posts.
- **Started:** Mid-2018.
- **Frontend History:** Python Flask -> Svelte (current: SvelteKit).
- **Current API Version (FastAPI):** v4.9.0.
- **V3 Implementation:** Modern parallel CRUD and Search endpoints under `/v3/crud`.
### API Versioning & Strategy
- `/crud` (v1): Legacy, still used by older frontend parts.
- `/v2/crud` (v2.5): Modern, preferred, and mostly functional endpoint.
- `/v3/crud`: The goal of this project phase. A new, parallel implementation with a refined structure and advanced search. **Runs alongside v1 and v2.**
### Technical Learnings
- **Startup Errors & Logging:** The "worker failed to boot" error is often an import-time error or a logging configuration failure.
- **Root Cause:** If `logging.config.dictConfig` fails (e.g., due to missing `/logs` directories in Docker), the entire application crashes.
- **Circular Dependencies during Refactoring:** Even deferred imports can trigger boot failures during FastAPI's introspection phase if the module structure is fragile. "Isolation Mode" (local definitions in routers) is a confirmed temporary fix.
- **V3 API Dependencies:** Standardized `Response` injection should use plain type hints (e.g., `response: Response`) to avoid router initialization failures.
- **Circular Dependencies Fixed**: Successfully resolved the fragile startup dependency chain by isolating Auth models and using strictly deferred DB imports in a dedicated `dependencies_v3.py` module.
- **Bootstrap Paradox Solved**: Implemented a guest-access exception for `site_domain` search, allowing the frontend to resolve site context without a JWT.
- **VS Code Optimization**: Configured workspace settings to suppress markdownlint noise and enforce 4-space indentation for cleaner documentation.
### V3 Architectural Progress (Jan 2026)
## Session Learnings & Progress (Jan 7, 2026) - MILESTONE
- **Modular Object Definitions:** Monolithic `ae_obj_types_def.py` refactored into domain-specific files in `app/object_definitions/`.
- **Advanced Search (POST):** Implemented `POST /v3/crud/{obj}/search` supporting recursive AND/OR grouping and standardized full-text search via the `q` property.
- **Security Hardening:** Implemented a 5-level recursion depth limit and a field allowlist (`searchable_fields`) for the Search API.
### 1. Stability & Architecture
- [x] **Permanent Dependency Fix**: Migrated `AccountContext` and Auth logic to dedicated modules (`auth_models.py`, `dependencies_v3.py`). This permanently resolved the "Worker failed to boot" issues.
- [x] **Modularized `lib_general.py`**: Successfully extracted core functionalities into specialized modules:
- `lib_email.py` (SMTP/Email)
- `lib_export.py` (CSV/Excel Exports)
- `lib_jwt.py` (JWT encoding/decoding)
- `lib_hash.py` (Argon2 hashing)
## Session Learnings & Progress (Jan 2-7, 2026)
### 2. V3 Search & Security
- [x] **Site Domain Exception**: Implemented and verified unauthenticated search for `site_domain`.
- [x] **Strict Multi-Tenant Isolation**: Enforced `account_id` filtering at the database level for all other V3 endpoints.
### V3 API Security Hardening (Jan 7, 2026) - MILESTONE
- **Mandatory JWT Authentication**: Successfully implemented strict multi-tenant isolation across all V3 CRUD and Search endpoints.
- **Account Isolation**: results are automatically filtered by `account_id` from the JWT.
- **Bootstrap Paradox Exception**: `site_domain` search is explicitly allowed for unauthenticated guests to unblock site context resolution.
### Unified Agent Architecture
- **Refined Specification**: Incorporated feedback from the Frontend Svelte agent. The Unified Agent will handle **Automated Schema Synchronization**, **Log Stream Aggregation**, and **Automated Lifecycle Management**.
### Infrastructure & Progress
- [x] **Modularize `lib_general.py`**: Successfully extracted Email, Export, JWT, and Hash functions into specialized modules (`lib_email.py`, `lib_export.py`, `lib_jwt.py`, `lib_hash.py`).
### 3. Unified Agent Platform
- [x] **Initialized `aether_platform`**: Created the orchestrator root at `/home/scott/OSIT_dev/aether_platform/`.
- [x] **Established Meta-Structure**: Linked `ae_api`, `ae_app`, and `ae_env` into the platform root via symbolic links.
- [x] **Unified Agent Specification**: Published and refined the `UNIFIED_AGENT_ARCH.md` incorporating frontend agent feedback.
## Current To-Do List
### 1. High Priority & Urgent
- [ ] **Initialize `aether_platform` Project** (Priority: High): Create the root directory at `/home/scott/OSIT_dev/aether_platform/` and establish the initial meta-structure.
- [ ] **Unified Agent Architecture Document** (Priority: High): Refine and synchronize the final spec (Draft Done).
- [ ] **Permanent Dependency Fix** (Priority: Urgent): Migrate `AccountContext` and Auth logic to a dedicated module.
- [ ] **Unified Agent Core Logic**: Plan the implementation of the orchestrator's cross-stack diagnostic tools.
- [ ] **Docker MCP Integration**: Re-attempt environment diagnostics using the correct python path (`./env_mcp/bin/python`).
### 2. Infrastructure & Environment
- [ ] **Docker MCP Integration**: Re-attempt diagnostics using the correct python path (`./env_mcp/bin/python`).
- [ ] **Agent Bridge Repair**: Resolve the `psutil` or syntax issues in `app/routers/agent_bridge.py`.
- [ ] **Nginx Configuration**: Resolve 404 errors on Port 8888 routes.
### 2. Infrastructure & Technical Debt
- [ ] **Agent Bridge Repair**: Fix the `psutil` or syntax issues in `agent_bridge.py`.
- [ ] **Nginx Configuration**: Finalize Port 8888 route proxying if needed.
### Workflow & Collaboration
- **`GEMINI.md` Strategy:** Context flows up the tree.
- **Agents Sync (rsync):** Shared documentation and notifications pushed to `~/agents_sync/`.
- **Home Server:** Remote proxy at `https://dev-api.oneskyit.com`.- [x] **Establish Symbolic Links**: Linked API, App, and Env into aether_platform.
- **Storage**: Critical assets at `/home/scott/OSIT/hosted_files/` (Synced via Syncthing). Files are often accessed directly via API download endpoints.
- **Agents Sync**: Shared documentation and notifications pushed to `~/agents_sync/`.

View File

@@ -1,11 +1,8 @@
"""
This file contains general utility functions and helpers specifically for API v3.
It aims to provide a clean slate for new methods and refactor existing ones from lib_general.py
that are relevant to the v3 API, while removing unused or outdated functionalities.
Refactored 2026-01-07 to move Auth logic to dependencies_v3.py to fix circular dependencies.
"""
# Standard library imports
import time
import logging
from typing import (
Any,
@@ -15,7 +12,6 @@ from typing import (
Union,
)
# Third-party imports
from fastapi import (
APIRouter,
Depends,
@@ -29,143 +25,23 @@ from fastapi import (
from pydantic import (
BaseModel,
Field,
ValidationError,
computed_field,
model_validator,
)
# Internal imports (from this project)
# Re-import from the new central auth models
from app.models.auth_models import AccountContext
# Import the dependency functions for backward compatibility in existing v3 routes
from app.routers.dependencies_v3 import (
get_account_context,
get_account_context_optional,
PaginationParams,
StatusFilterParams,
SerializationParams,
DelayParams
)
from app.config import settings
from app.db_sql import redis_lookup_id_random
from app.log import get_logger
logger = get_logger(__name__)
# --- Pydantic Model for Account Context ---
class AccountContext(BaseModel):
account_id: Optional[int]
account_id_random: Optional[str]
# --- Dependency Function for Account Context ---
def get_account_context(
x_account_id: Optional[str] = Header(None, min_length=11, max_length=22),
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100), # Assuming 'bypass' or similar string
x_no_account_id_token: Optional[str] = Query(None, min_length=11, max_length=22),
) -> AccountContext:
"""
Resolves the account context from headers/query parameters with defined precedence.
Precedence: x_account_id (header) > x_no_account_id_token (query) > x_no_account_id (header flag)
Raises HTTPException 403 if no valid account is found and no bypass is indicated.
"""
logger.setLevel(logging.DEBUG) # Adjust as needed
logger.debug(locals())
resolved_account_id = None
resolved_account_id_random = None
if x_account_id:
# Primary check: x_account_id header
resolved_account_id_random = x_account_id
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_account_id):
resolved_account_id = looked_up_id
logger.info(f'Found account from x_account_id header: {resolved_account_id}')
else:
logger.warning(f'Invalid x_account_id header provided: {x_account_id}')
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Invalid X-Account-ID header.')
elif x_no_account_id_token:
# Secondary check: x_no_account_id_token query parameter
resolved_account_id_random = x_no_account_id_token
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_no_account_id_token):
resolved_account_id = looked_up_id
logger.info(f'Found account from x_no_account_id_token query: {resolved_account_id}')
else:
logger.warning(f'Invalid x_no_account_id_token query provided: {x_no_account_id_token}')
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Invalid X-No-Account-ID-Token query parameter.')
elif x_no_account_id:
# Tertiary check: x_no_account_id header for bypass
# For now, just presence indicates bypass. Can add a specific value check later if needed.
logger.info(f'X-No-Account-ID header found: {x_no_account_id}. Proceeding without specific account context.')
resolved_account_id = None # Explicitly None for "no specific account"
resolved_account_id_random = '--- NO ACCOUNT ---'
else:
logger.warning('No valid account context provided via X-Account-ID, X-No-Account-ID-Token, or X-No-Account-ID.')
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Account context required. Please provide X-Account-ID, X-No-Account-ID-Token, or X-No-Account-ID.')
return AccountContext(account_id=resolved_account_id, account_id_random=resolved_account_id_random)
# --- Pydantic Model for Pagination ---
class PaginationParams(BaseModel):
limit: int = 100 # Default limit
offset: int = 0
# --- Dependency Function for Pagination ---
def get_pagination_params(
limit: int = Query(100, ge=0, description="Maximum number of items to return"),
offset: int = Query(0, ge=0, description="Number of items to skip (for pagination)"),
) -> PaginationParams:
return PaginationParams(limit=limit, offset=offset)
# --- Pydantic Model for Status Filtering ---
class StatusFilterParams(BaseModel):
enabled: str = 'enabled' # 'enabled', 'disabled', 'all'
hidden: str = 'not_hidden' # 'hidden', 'not_hidden', 'all'
# --- Dependency Function for Status Filtering ---
def get_status_filter_params(
enabled: str = Query('enabled', description="Filter by object enabled status ('enabled', 'disabled', 'all')"),
hidden: str = Query('not_hidden', description="Filter by object hidden status ('hidden', 'not_hidden', 'all')"),
) -> StatusFilterParams:
allowed_enabled_values = {'enabled', 'disabled', 'all'}
allowed_hidden_values = {'hidden', 'not_hidden', 'all'}
if enabled not in allowed_enabled_values:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid value for 'enabled'. Must be one of {list(allowed_enabled_values)}."
)
if hidden not in allowed_hidden_values:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid value for 'hidden'. Must be one of {list(allowed_hidden_values)}."
)
return StatusFilterParams(enabled=enabled, hidden=hidden)
# --- Pydantic Model for Serialization Options ---
class SerializationParams(BaseModel):
by_alias: bool = True
exclude_unset: bool = False
exclude_defaults: bool = False # Added based on common_route_params
exclude_none: bool = False # Added based on common_route_params
# --- Dependency Function for Serialization Options ---
def get_serialization_params(
by_alias: bool = Query(True, description="Whether to use field aliases for serialization"),
exclude_unset: bool = Query(False, description="Whether to exclude unset fields from the response"),
exclude_defaults: bool = Query(False, description="Whether to exclude fields with their default values from the response"),
exclude_none: bool = Query(False, description="Whether to exclude fields that are None from the response"),
) -> SerializationParams:
return SerializationParams(
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
# --- Pydantic Model for Delay ---
class DelayParams(BaseModel):
sleep_time_ms: int = 0 # Raw delay value in ms
sleep_time_s: float = 0.0 # Converted to seconds for time.sleep()
# --- Dependency Function for Delay ---
def get_delay_params(
x_delay_ms: Optional[int] = Header(0, alias='X-Delay-ms', description="Delay response for X milliseconds (header)"),
delay_ms: Optional[int] = Query(0, description="Delay response for X milliseconds (query parameter)"),
) -> DelayParams:
calculated_delay_ms = max(x_delay_ms or 0, delay_ms or 0)
return DelayParams(sleep_time_ms=calculated_delay_ms, sleep_time_s=calculated_delay_ms / 1000.0)
# Note: Dependency function implementations have moved to app/routers/dependencies_v3.py

13
app/models/auth_models.py Normal file
View File

@@ -0,0 +1,13 @@
from typing import Optional
from pydantic import BaseModel
# Zero-dependency auth models for V3
# Created 2026-01-07 to resolve circular dependencies in FastAPI startup
class AccountContext(BaseModel):
account_id: Optional[int]
account_id_random: Optional[str]
administrator: bool = False
manager: bool = False
super: bool = False
auth_method: str = 'legacy_header'

View File

@@ -4,38 +4,15 @@ import json
import urllib.parse
import time
import asyncio
from pydantic import BaseModel
import logging
log = logging.getLogger(__name__)
# NOTE: We are defining these locally to avoid circular dependency hell with lib_general_v3 for now.
class AccountContext(BaseModel):
account_id: Optional[int]
account_id_random: Optional[str]
administrator: bool = False
manager: bool = False
super: bool = False
auth_method: str = 'legacy_header'
class PaginationParams(BaseModel):
limit: int = 100
offset: int = 0
class StatusFilterParams(BaseModel):
enabled: str = 'enabled'
hidden: str = 'not_hidden'
class SerializationParams(BaseModel):
by_alias: bool = True
exclude_unset: bool = False
exclude_defaults: bool = False
exclude_none: bool = False
class DelayParams(BaseModel):
sleep_time_ms: int = 0
sleep_time_s: float = 0.0
from app.lib_general_v3 import (
AccountContext, get_account_context, get_account_context_optional,
PaginationParams, StatusFilterParams,
SerializationParams, DelayParams
)
from app.models.response_models import *
from app.models.api_crud_models import SearchFilter, SearchQuery
from app.ae_obj_types_def import obj_type_kv_li
@@ -43,82 +20,6 @@ from app.ae_obj_types_def import obj_type_kv_li
router = APIRouter()
# --- Local Dependencies ---
def get_account_context_optional(
x_account_id: Optional[str] = Header(None, min_length=11, max_length=22),
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100),
x_no_account_id_token: Optional[str] = Query(None, min_length=11, max_length=22),
) -> AccountContext:
from app.db_sql import redis_lookup_id_random
resolved_account_id = None
resolved_account_id_random = None
auth_method = 'guest'
if x_account_id:
resolved_account_id_random = x_account_id
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_account_id):
resolved_account_id = looked_up_id
auth_method = 'legacy_header'
elif x_no_account_id_token:
resolved_account_id_random = x_no_account_id_token
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_no_account_id_token):
resolved_account_id = looked_up_id
auth_method = 'token_query'
elif x_no_account_id:
resolved_account_id = None
resolved_account_id_random = '--- NO ACCOUNT ---'
auth_method = 'bypass'
return AccountContext(
account_id=resolved_account_id,
account_id_random=resolved_account_id_random,
auth_method=auth_method,
administrator=(auth_method == 'bypass'),
manager=(auth_method == 'bypass'),
super=(auth_method == 'bypass')
)
def get_account_context(
x_account_id: Optional[str] = Header(None, min_length=11, max_length=22),
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100),
x_no_account_id_token: Optional[str] = Query(None, min_length=11, max_length=22),
) -> AccountContext:
ctx = get_account_context_optional(x_account_id, x_no_account_id, x_no_account_id_token)
if ctx.auth_method == 'guest':
# Raise strict 403 if required
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Account context required.')
return ctx
def get_pagination_params(
limit: int = Query(100, ge=0),
offset: int = Query(0, ge=0),
) -> PaginationParams:
return PaginationParams(limit=limit, offset=offset)
def get_status_filter_params(
enabled: str = Query('enabled'),
hidden: str = Query('not_hidden'),
) -> StatusFilterParams:
return StatusFilterParams(enabled=enabled, hidden=hidden)
def get_serialization_params(
by_alias: bool = Query(True),
exclude_unset: bool = Query(False),
exclude_defaults: bool = Query(False),
exclude_none: bool = Query(False),
) -> SerializationParams:
return SerializationParams(by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none)
def get_delay_params(
x_delay_ms: Optional[int] = Header(0, alias='X-Delay-ms'),
delay_ms: Optional[int] = Query(0),
) -> DelayParams:
val = max(x_delay_ms or 0, delay_ms or 0)
return DelayParams(sleep_time_ms=val, sleep_time_s=val / 1000.0)
# --- Helpers ---
def check_account_access(sql_result: Any, account: AccountContext, obj_name: str = None) -> bool:
@@ -176,7 +77,12 @@ def filter_order_by(order_by_li: Any, model: Any, table_name: str = None) -> Opt
def get_supported_filters(model: Any, status_filter: StatusFilterParams) -> StatusFilterParams:
if not model or not hasattr(model, "__fields__"):
return status_filter
adjusted = StatusFilterParams(enabled=status_filter.enabled, hidden=status_filter.hidden)
# We create a new instance to avoid side effects on the dependency object
from app.routers.dependencies_v3 import StatusFilterParams as SF
adjusted = SF()
adjusted.enabled = status_filter.enabled
adjusted.hidden = status_filter.hidden
if 'enable' not in model.__fields__:
adjusted.enabled = 'all'
if 'hide' not in model.__fields__:
@@ -192,7 +98,7 @@ def safe_json_loads(json_str: Optional[str]) -> Any:
# --- Routes ---
@router.get("/health", response_model=Resp_Body_Base)
async def health_check(delay: DelayParams = Depends(get_delay_params)):
async def health_check(delay: DelayParams = Depends()):
if delay.sleep_time_s > 0: await asyncio.sleep(delay.sleep_time_s)
return mk_resp(data={"status": "V3 API is healthy!"})
@@ -252,8 +158,8 @@ async def get_obj(
obj_id: str = Path(min_length=11, max_length=22),
view: str = Query('default'),
account: AccountContext = Depends(get_account_context),
serialization: SerializationParams = Depends(get_serialization_params),
delay: DelayParams = Depends(get_delay_params),
serialization: SerializationParams = Depends(),
delay: DelayParams = Depends(),
):
from app.db_sql import redis_lookup_id_random, sql_select
@@ -293,10 +199,10 @@ async def get_obj_li(
order_by_li: Optional[str] = None,
jp: Optional[Union[str, None]] = None,
account: AccountContext = Depends(get_account_context),
pagination: PaginationParams = Depends(get_pagination_params),
status_filter: StatusFilterParams = Depends(get_status_filter_params),
serialization: SerializationParams = Depends(get_serialization_params),
delay: DelayParams = Depends(get_delay_params),
pagination: PaginationParams = Depends(),
status_filter: StatusFilterParams = Depends(),
serialization: SerializationParams = Depends(),
delay: DelayParams = Depends(),
):
from app.db_sql import redis_lookup_id_random, sql_select
@@ -397,10 +303,10 @@ async def search_obj_li(
view: str = Query('default'),
order_by_li: Optional[str] = Query(None),
account: AccountContext = Depends(get_account_context_optional),
pagination: PaginationParams = Depends(get_pagination_params),
status_filter: StatusFilterParams = Depends(get_status_filter_params),
serialization: SerializationParams = Depends(get_serialization_params),
delay: DelayParams = Depends(get_delay_params),
pagination: PaginationParams = Depends(),
status_filter: StatusFilterParams = Depends(),
serialization: SerializationParams = Depends(),
delay: DelayParams = Depends(),
):
from app.db_sql import redis_lookup_id_random, sql_select
@@ -487,8 +393,8 @@ async def post_obj(
obj_type_l1: str = Path(min_length=2, max_length=50),
return_obj: Optional[bool] = True,
account: AccountContext = Depends(get_account_context),
serialization: SerializationParams = Depends(get_serialization_params),
delay: DelayParams = Depends(get_delay_params),
serialization: SerializationParams = Depends(),
delay: DelayParams = Depends(),
):
from app.db_sql import sql_insert, get_id_random, sql_select
@@ -542,8 +448,8 @@ async def patch_obj(
obj_id: str = Path(min_length=11, max_length=22),
return_obj: Optional[bool] = True,
account: AccountContext = Depends(get_account_context),
serialization: SerializationParams = Depends(get_serialization_params),
delay: DelayParams = Depends(get_delay_params),
serialization: SerializationParams = Depends(),
delay: DelayParams = Depends(),
):
from app.db_sql import redis_lookup_id_random, sql_select, sql_update
@@ -590,7 +496,7 @@ async def delete_obj(
obj_id: str = Path(min_length=11, max_length=22),
method: str = Query('delete', regex='^(delete|hide|disable)$'),
account: AccountContext = Depends(get_account_context),
delay: DelayParams = Depends(get_delay_params),
delay: DelayParams = Depends(),
):
from app.db_sql import redis_lookup_id_random, sql_select, sql_update, sql_delete
@@ -638,10 +544,10 @@ async def get_child_obj_li(
order_by_li: Optional[str] = None,
jp: Optional[Union[str, None]] = None,
account: AccountContext = Depends(get_account_context),
pagination: PaginationParams = Depends(get_pagination_params),
status_filter: StatusFilterParams = Depends(get_status_filter_params),
serialization: SerializationParams = Depends(get_serialization_params),
delay: DelayParams = Depends(get_delay_params),
pagination: PaginationParams = Depends(),
status_filter: StatusFilterParams = Depends(),
serialization: SerializationParams = Depends(),
delay: DelayParams = Depends(),
):
from app.db_sql import redis_lookup_id_random, sql_select
@@ -727,8 +633,8 @@ async def post_child_obj(
child_obj_type: str = Path(min_length=2, max_length=50),
return_obj: Optional[bool] = True,
account: AccountContext = Depends(get_account_context),
serialization: SerializationParams = Depends(get_serialization_params),
delay: DelayParams = Depends(get_delay_params),
serialization: SerializationParams = Depends(),
delay: DelayParams = Depends(),
):
from app.db_sql import redis_lookup_id_random, sql_select, sql_insert, get_id_random
@@ -791,8 +697,8 @@ async def get_child_obj(
child_obj_type: str = Path(min_length=2, max_length=50),
child_obj_id: str = Path(min_length=11, max_length=22),
account: AccountContext = Depends(get_account_context),
serialization: SerializationParams = Depends(get_serialization_params),
delay: DelayParams = Depends(get_delay_params),
serialization: SerializationParams = Depends(),
delay: DelayParams = Depends(),
):
from app.db_sql import redis_lookup_id_random, sql_select
@@ -827,8 +733,8 @@ async def patch_child_obj(
child_obj_id: str = Path(min_length=11, max_length=22),
return_obj: Optional[bool] = True,
account: AccountContext = Depends(get_account_context),
serialization: SerializationParams = Depends(get_serialization_params),
delay: DelayParams = Depends(get_delay_params),
serialization: SerializationParams = Depends(),
delay: DelayParams = Depends(),
):
from app.db_sql import redis_lookup_id_random, sql_select, sql_update
@@ -870,7 +776,7 @@ async def delete_child_obj(
child_obj_id: str = Path(min_length=11, max_length=22),
method: str = Query('delete', regex='^(delete|hide|disable)$'),
account: AccountContext = Depends(get_account_context),
delay: DelayParams = Depends(get_delay_params),
delay: DelayParams = Depends(),
):
from app.db_sql import redis_lookup_id_random, sql_select, sql_update, sql_delete

View File

@@ -0,0 +1,104 @@
from fastapi import Depends, Header, HTTPException, Query, Response, status
from typing import Optional, Union
import logging
import asyncio
from app.models.auth_models import AccountContext
log = logging.getLogger(__name__)
# --- Account Context Dependencies ---
def get_account_context_optional(
x_account_id: Optional[str] = Header(None, min_length=11, max_length=22),
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100),
x_no_account_id_token: Optional[str] = Query(None, min_length=11, max_length=22),
) -> AccountContext:
"""
Resolves the account context but does not raise 403 on failure.
Uses DEFERRED imports to prevent circular dependency at startup.
"""
from app.db_sql import redis_lookup_id_random
resolved_account_id = None
resolved_account_id_random = None
auth_method = 'guest'
if x_account_id:
resolved_account_id_random = x_account_id
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_account_id):
resolved_account_id = looked_up_id
auth_method = 'legacy_header'
elif x_no_account_id_token:
resolved_account_id_random = x_no_account_id_token
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_no_account_id_token):
resolved_account_id = looked_up_id
auth_method = 'token_query'
elif x_no_account_id:
resolved_account_id = None
resolved_account_id_random = '--- NO ACCOUNT ---'
auth_method = 'bypass'
return AccountContext(
account_id=resolved_account_id,
account_id_random=resolved_account_id_random,
auth_method=auth_method,
administrator=(auth_method == 'bypass'),
manager=(auth_method == 'bypass'),
super=(auth_method == 'bypass')
)
def get_account_context(
x_account_id: Optional[str] = Header(None, min_length=11, max_length=22),
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100),
x_no_account_id_token: Optional[str] = Query(None, min_length=11, max_length=22),
) -> AccountContext:
"""Strict version of account context resolution."""
ctx = get_account_context_optional(x_account_id, x_no_account_id, x_no_account_id_token)
if ctx.auth_method == 'guest':
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Account context required.')
return ctx
# --- Shared Pagination & Status Dependencies ---
class PaginationParams:
def __init__(
self,
limit: int = Query(100, ge=0),
offset: int = Query(0, ge=0),
):
self.limit = limit
self.offset = offset
class StatusFilterParams:
def __init__(
self,
enabled: str = Query('enabled'),
hidden: str = Query('not_hidden'),
):
self.enabled = enabled
self.hidden = hidden
class SerializationParams:
def __init__(
self,
by_alias: bool = Query(True),
exclude_unset: bool = Query(False),
exclude_defaults: bool = Query(False),
exclude_none: bool = Query(False),
):
self.by_alias = by_alias
self.exclude_unset = exclude_unset
self.exclude_defaults = exclude_defaults
self.exclude_none = exclude_none
class DelayParams:
def __init__(
self,
x_delay_ms: Optional[int] = Header(0, alias='X-Delay-ms'),
delay_ms: Optional[int] = Query(0),
):
val = max(x_delay_ms or 0, delay_ms or 0)
self.sleep_time_ms = val
self.sleep_time_s = val / 1000.0

View File

@@ -48,6 +48,7 @@ The **Unified Aether AI Agent** is a single, cohesive AI entity designed to elim
### F. Storage Layer (Syncthing / Hosted Files)
* **Location:** `/home/scott/OSIT/hosted_files/` (Main Workstation) and synchronized Remote Servers.
* **Role:** Extremely important persistent storage for files served via the API (e.g., `hosted_file`, `event_file`).
* **API-Driven Access:** Files are frequently accessed/downloaded directly via API endpoints. The agent must understand the routing and security logic (JWT validation) governing these downloads.
* **Synchronization:** Managed via **Syncthing** (similar to the `agents_sync` directory), ensuring real-time mirroring across the Aether ecosystem.
* **Agent Access Requirements:**
* Full filesystem access to the local hosted files directory.

50
verify_v3_exceptions.py Normal file
View File

@@ -0,0 +1,50 @@
import requests
import json
# Configuration
BASE_URL = "https://dev-api.oneskyit.com"
SEARCH_ENDPOINT = f"{BASE_URL}/v3/crud/site_domain/search"
RESTRICTED_ENDPOINT = f"{BASE_URL}/v3/crud/journal/search"
def test_site_domain_exception():
print("--- Testing site_domain guest access (Exception) ---")
search_query = {
"q": "%", # Match all for testing
"and": []
}
try:
# No Authorization or X-Account-ID headers provided
response = requests.post(SEARCH_ENDPOINT, json=search_query)
print(f"Status Code: {response.status_code}")
if response.status_code == 200:
data = response.json()
print("SUCCESS: site_domain search allowed without authentication.")
print(f"Result count: {len(data.get('data', []))}")
else:
print(f"FAILED: site_domain search returned {response.status_code}")
print(response.text)
except Exception as e:
print(f"Error during site_domain test: {e}")
def test_restricted_search():
print("\n--- Testing restricted search (Should fail) ---")
search_query = {"q": "%"}
try:
response = requests.post(RESTRICTED_ENDPOINT, json=search_query)
print(f"Status Code: {response.status_code}")
if response.status_code == 403:
print("SUCCESS: Restricted search was correctly blocked (403 Forbidden).")
else:
print(f"FAILED: Restricted search returned {response.status_code} instead of 403.")
except Exception as e:
print(f"Error during restricted test: {e}")
if __name__ == "__main__":
test_site_domain_exception()
test_restricted_search()