jpmschweitzerandClaude Sonnet 5 99ee994023 feat(tenants): add the tenant registry, the one enforcement point Book and Scribe now share (T-8)
A tenant has been an unchecked string since T-4/T-7: the Office's
X-Office-Tenant header or a user JWT's tatlock_user claim, with Book and
Scribe each running their own _require_tenant that checked only presence.
A typo'd claim opened a fresh, empty namespace rather than being refused.

src/domains/tenants/ (controller/models/schemas/service) adds tenants and
tenant_consumers (migration 007, down_revision="006"): tenants.id is the
slug a tatlock tenant carries, kind (household|person|test) and status
(active|suspended) CHECK-constrained; tenant_consumers is the grant pairing
a consumer to a tenant. Migration 007 seeds the two tenants in use today —
jpmschweitzer (household, "Jeroen") and llm_tester (test, "Playground") —
each granted to office-claude, the only Office consumer configured in
Authentik. Every existing owner_tenant column (book_records,
book_tenant_counters, scribe_addresses, scribe_messages,
scribe_mailbox_entries — five, grepped and confirmed) now carries a
foreign key to tenants.id, ON DELETE RESTRICT, at both the migration and
the ORM model.

TenantsService.require_tenant is the one enforcement point: no tenant is
400 tenant_required (unchanged); an unknown tenant is 403 tenant_unknown;
a suspended one is 403 tenant_suspended; an Office caller whose consumer
holds no grant in that tenant is 403 consumer_not_granted. One query — a
LEFT JOIN against the grant — so presence, status and the grant are all
read in a single round trip. Book's and Scribe's own _require_tenant now
delegate to it, translating TenantError to their own exception type so
every call site and each controller's single except clause keep working
unchanged.

Read-only this round: GET /tenants/me answers the caller's own tenant and
its granted consumers (the cheap "am I allowed here" probe); GET /tenants
answers every tenant for a tatlock/scheduler caller, or a list of one for
a tenant-bearing caller — another tenant's existence is never revealed.
No write routes; provisioning a tenant is workspace T-195.

Tests: tests/test_tenants.py and tests/test_tenants_migration.py (new);
a TestTenantRegistryEnforcement class added to both tests/test_book.py and
tests/test_scribe.py proving all four cases (unknown, suspended,
ungranted-consumer, and the control) on a live Book and Scribe route, not
only on /tenants itself. tests/conftest.py gains a tenant_registry schema
fixture and a suspended_tenant fixture (inserted and deleted per test; no
FK references it); book_schema and scribe_schema each independently grant
a second, test-environment-only consumer (office-gemini) in both seeded
tenants, since test_book.py and test_scribe.py have used it as a stand-in
second consumer since before this grant check existed — the migration
itself still seeds only office-claude, matching the dossier.

Mutation-checked: dropping the grant check fails the three
consumer_not_granted tests (200 instead of 403); dropping the status
check fails the three tenant_suspended tests; making Book bypass the
helper fails its unknown-tenant test (200 instead of 403). All three
restored and the full suite re-verified green (508 + 35 = 543 passed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-19 17:30:55 +02:00
2026-01-04 21:51:21 +01:00

Core Code API

Central API service providing infrastructure management, home automation, and utility endpoints for the homelab ecosystem.

Features

Infrastructure Management

  • Portainer Integration: Stack and container management
  • NPM Integration: Nginx Proxy Manager domain and certificate management
  • Service Control: Start/stop services with container orchestration

Home Automation (Housekeeping API)

  • Device Control: Turn on/off, toggle, and set brightness for smart devices
  • Scene Activation: Trigger Home Assistant scenes
  • Script Execution: Run Home Assistant scripts
  • Automation Management: Enable/disable automations
  • State History: Query device state changes over time
  • Area Discovery: List rooms and areas

Utilities

  • DNS Lookup: Query DNS records (A, AAAA, MX, TXT, CNAME, NS, SOA, PTR)
  • Health Checks: Service health monitoring with database connectivity status

Architecture

src/
├── config.py                      # Global application settings
├── logging_config.py              # Logging configuration
├── base_controller.py             # Base controller pattern
├── main.py                        # FastAPI application entry point
├── auth/
│   └── oidc.py                    # OIDC authentication
├── clients/
│   ├── homeassistant_client.py    # Home Assistant REST client
│   ├── npm_client.py              # Nginx Proxy Manager client
│   └── portainer_client.py        # Portainer API client
├── controllers/
│   ├── health_controller.py       # Health endpoints
│   ├── housekeeping_controller.py # Home automation endpoints
│   ├── infrastructure_controller.py # Infrastructure management
│   ├── static_controller.py       # Static file serving
│   └── tools_controller.py        # DNS and utility tools
└── dns/
    ├── service.py                 # DNS lookup service
    └── exceptions.py              # DNS-specific exceptions

API Endpoints

Health

  • GET / - Service info and documentation links
  • GET /health - Basic health status
  • GET /health/full - Detailed component health
  • GET /health/diagnostics - Full diagnostic information

Infrastructure (/infrastructure)

  • GET /infrastructure/health - Portainer/NPM connection status
  • GET /infrastructure/services - List all services (stacks)
  • GET /infrastructure/services/{name} - Get service details
  • GET /infrastructure/services/{name}/status - Service status
  • POST /infrastructure/services/{name}/start - Start service
  • POST /infrastructure/services/{name}/stop - Stop service
  • GET /infrastructure/containers - List containers
  • GET /infrastructure/containers/{name} - Container details
  • GET /infrastructure/containers/{name}/logs - Container logs
  • GET /infrastructure/ports - List exposed ports
  • GET /infrastructure/domains - List proxy domains
  • GET /infrastructure/widget-data - Dashboard widget data

Housekeeping (/housekeeping)

  • GET /housekeeping/health - Home Assistant connection status
  • GET /housekeeping/devices - List controllable devices
  • GET /housekeeping/devices/{entity_id} - Device details
  • POST /housekeeping/devices/{entity_id}/control - Control device
  • GET /housekeeping/scenes - List scenes
  • POST /housekeeping/scenes/{scene_id}/activate - Activate scene
  • GET /housekeeping/scripts - List scripts
  • POST /housekeeping/scripts/{script_id}/run - Run script
  • GET /housekeeping/automations - List automations
  • POST /housekeeping/automations/{automation_id}/toggle - Toggle automation
  • GET /housekeeping/history - State history
  • GET /housekeeping/areas - List areas/rooms

Tools (/tools)

  • POST /tools/dns/lookup - DNS record lookup

Development

Requirements

  • Python 3.12+
  • Docker (for containerized deployment)

Local Development

# Install dependencies
pip install -r requirements.txt

# Run locally
uvicorn src.main:app --reload --host 0.0.0.0 --port 8083

Running Tests

# Run all tests
pytest

# Run with coverage
pytest --cov=src --cov-report=term-missing

# Run specific test file
pytest tests/test_housekeeping.py -v

Adding New Dependencies

Dependencies use major version pinning (~=) for automatic patch updates while preventing breaking changes.

  1. Add package to requirements.txt with major version constraint:

    package-name~=1.2.0  # Allows 1.2.x, blocks 1.3.0
    
  2. Restart the container to install:

    docker restart core-api
    

Docker Build

# Build image
docker build -t core-code:latest .

# Run container
docker run -p 8083:8083 core-code:latest

Deployment

Portainer Stack

  1. Navigate to Portainer UI
  2. Go to Stacks → Add Stack
  3. Name: core-code
  4. Upload stacks/core-code.yml or paste contents
  5. Deploy

Environment Variables

Variable Description Default
PORTAINER_URL Portainer API URL http://localhost:9000
PORTAINER_API_KEY Portainer API key -
NPM_URL Nginx Proxy Manager URL http://localhost:81
NPM_EMAIL NPM admin email -
NPM_PASSWORD NPM admin password -
HOMEASSISTANT_URL Home Assistant URL http://localhost:8123
HOMEASSISTANT_TOKEN HA long-lived access token -
OIDC_ENABLED Enable OIDC auth false
OIDC_ISSUERS OIDC issuer URLs (comma-separated) See config.py
OIDC_AUDIENCES OIDC audiences (comma-separated) See config.py

API Documentation

Once deployed, access documentation at:

Health Checks

  • Basic: GET /health - Fast liveness check for container orchestration
  • Full: GET /health/full - Returns database status (503 if unhealthy)
  • Diagnostics: GET /health/diagnostics - Service info and configuration

Security

  • Runs as non-root user (uid 1000)
  • OIDC authentication support via Authentik
  • CORS configured for same-network access
  • Admin endpoints require authentication when OIDC enabled

License

Internal use only.

S
Description
the core system management api for the tower-of-joy system.
Readme
1,014 KiB
2026-10-20 00:57:58 +02:00
Languages
Python 96.8%
HTML 2.4%
Makefile 0.4%
Shell 0.3%