# Azaion Admin API — Architecture ## 1. System Context **Problem being solved**: Azaion Suite requires a centralized admin API to manage users, assign roles, and securely distribute encrypted software resources (DLLs, AI models, installers) to authorized devices and SaaS users. **System boundaries**: - **Inside**: User management, authentication (JWT), role-based authorization, file-based resource storage (upload / list / clear). - **Outside**: Client applications (admin web panel at admin.azaion.com, fTPM-secured Jetson edge devices), PostgreSQL database, server filesystem for resource storage. > **Note (AZ-197, 2026-05-13)**: hardware-fingerprint binding (`User.Hardware`, `CheckHardwareHash`, `PUT /users/hardware/set`, `POST /resources/check`, `HardwareIdMismatch`/`BadHardware` error codes) was removed. Edge devices now ship as fTPM-secured Jetsons; server/desktop access is SaaS-only. The `User.Hardware` DB column remains as a nullable tombstone (no migration in AZ-197). > **Note (cycle 2, 2026-05-14)**: the encrypted resource download (`POST /resources/get/{dataFolder?}`) and both installer endpoints (`GET /resources/get-installer`, `GET /resources/get-installer/stage`) were removed as obsolete. Their orphaned support code went with them: `ResourcesService.GetEncryptedResource` / `GetInstaller`, `Security.GetApiEncryptionKey` / `EncryptTo` / `DecryptTo`, the `GetResourceRequest` DTO (+ `WrongResourceName` error code 50, gap kept), and the `ResourcesConfig.SuiteInstallerFolder` / `SuiteStageInstallerFolder` properties + their env var rows in every config artifact. The `Azaion.Test` unit-test project became empty and was removed from the solution. Per-user file encryption is no longer part of the system; resource delivery is now upload + list + clear only. ADR-003 below is **retired** as a result. **External systems**: | System | Integration Type | Direction | Purpose | |--------|-----------------|-----------|---------| | PostgreSQL | Database (linq2db) | Both | User data persistence | | Server filesystem | File I/O | Both | Resource file storage and retrieval | | Azaion Suite client | REST API | Inbound | Resource download, login | | Admin web panel (admin.azaion.com) | REST API | Inbound | User management, resource upload | ## 2. Technology Stack | Layer | Technology | Version | Rationale | |-------|-----------|---------|-----------| | Language | C# | .NET 10.0 | Modern, cross-platform, strong typing | | Framework | ASP.NET Core Minimal API | 10.0 | Lightweight, minimal boilerplate | | Database | PostgreSQL | (server-side) | Open-source, robust relational DB | | ORM | linq2db | 5.4.1 | Lightweight, LINQ-native, no migrations overhead | | Cache | LazyCache (in-memory) | 2.4.0 | Simple async caching for user lookups | | Auth | JWT Bearer | 10.0.3 | Stateless token authentication | | Validation | FluentValidation | 11.3.0 / 11.10.0 | Declarative request validation | | Logging | Serilog | 4.1.0 | Structured logging (console + file) | | API Docs | Swashbuckle (Swagger) | 10.1.4 | OpenAPI specification | | Serialization | Newtonsoft.Json | 13.0.1 | JSON for DB field mapping and responses | | Container | Docker | .NET 10.0 images | Multi-stage build, ARM64 support | | CI/CD | Woodpecker CI | — | Branch-based ARM64 builds | | Registry | docker.azaion.com | — | Private container registry | ## 3. Deployment Model **Environments**: Development (local), Production (Linux server) **Infrastructure**: - Self-hosted Linux server (evidenced by `env/` provisioning scripts for Debian/Ubuntu) - Docker containerization with private registry (`docker.azaion.com`, `localhost:5000`) - No orchestration (single container deployment via `deploy.cmd`) **Environment-specific configuration**: | Config | Development | Production | |--------|-------------|------------| | Database | Local PostgreSQL (port 4312) | Remote PostgreSQL (same custom port) | | Secrets | Environment variables (`ASPNETCORE_*`) | Environment variables | | Logging | Console + file | Console + rolling file (`logs/log.txt`) | | Swagger | Enabled | Disabled | | CORS | Same as prod | `admin.azaion.com` | ## 4. Data Model Overview **Core entities**: | Entity | Description | Owned By Component | |--------|-------------|--------------------| | User | System user with email (UNIQUE-indexed via `users_email_uidx`), password hash, role, config (legacy `Hardware` column tombstoned per AZ-197). Subset of users have `Role = CompanionPC` and are auto-provisioned via `POST /devices` (AZ-196), which delegates the insert to `UserService.RegisterUser` (post-security-audit consolidation, finding F-3). | 01 Data Layer | | UserConfig | JSON-serialized per-user configuration (queue offsets) | 01 Data Layer | | RoleEnum | Authorization role hierarchy (None → ApiAdmin); `ResourceUploader` retained as data only after the OTA endpoints were retired | 01 Data Layer | | DetectionClass *(AZ-513, cycle 1)* | Operator-managed detection-class catalogue (Name, ShortName, Color, MaxSizeM, PhotoMode?) backing the UI Detection Classes table | 01 Data Layer | | ExceptionEnum | Business error code catalog (HW-related codes 40/45 removed by AZ-197) | Common Helpers | > **Removed in cycle 1 / post-cycle-1**: the `Resource` entity, the `resources` table, and the OTA delivery flow (AZ-183 — F10) were reverted after the security audit (finding F-1). The data model no longer carries an OTA-artifact entity. **Key relationships**: - User → RoleEnum: each user has exactly one role - User → UserConfig: optional 1:1 JSON field containing queue offsets **Data flow summary**: - Client → API → UserService → PostgreSQL: user CRUD operations - Client → API → ResourcesService → Filesystem: resource upload / list / clear (encrypted download + installer delivery were retired in cycle 2) ## 5. Integration Points ### Internal Communication | From | To | Protocol | Pattern | Notes | |------|----|----------|---------|-------| | Admin API | User Management | Direct DI call | Request-Response | Scoped service injection | | Admin API | Auth & Security | Direct DI call | Request-Response | Scoped service injection | | Admin API | Resource Management | Direct DI call | Request-Response | Scoped service injection | | User Management | Data Layer | Direct DI call | Request-Response | Singleton DbFactory | | Auth & Security | User Management | Direct DI call | Request-Response | IUserService.GetByEmail | ### External Integrations | External System | Protocol | Auth | Rate Limits | Failure Mode | |----------------|----------|------|-------------|--------------| | PostgreSQL | TCP (Npgsql) | Username/password | None configured | Exception propagation | | Filesystem | OS I/O | OS-level permissions | None | Exception propagation | ## 6. Non-Functional Requirements | Requirement | Target | Measurement | Priority | |------------|--------|-------------|----------| | Max upload size | 200 MB | Kestrel MaxRequestBodySize | High | | Password hashing | SHA-384 | Per-user | Medium | | Cache TTL | 4 hours | User entity cache | Low | > The "File encryption / AES-256-CBC" NFR was retired in cycle 2 along with the encrypted-download endpoint. See ADR-003. No explicit availability, latency, throughput, or recovery targets found in the codebase. ## 7. Security Architecture **Authentication**: JWT Bearer tokens (HMAC-SHA256 signed, validated for issuer/audience/lifetime/signing key). **Authorization**: Role-based (RBAC) via ASP.NET Core authorization policies: - `apiAdminPolicy` — requires `ApiAdmin` role - General `[Authorize]` — any authenticated user > The `apiUploaderPolicy` was added by AZ-183 and removed in the post-cycle-1 revert along with the OTA endpoints it guarded. `RoleEnum.ResourceUploader` remains as data only. **Data protection**: - At rest: resource files are stored as plain bytes on the server filesystem (per-user AES-256-CBC encryption was retired in cycle 2 — see ADR-003). - In transit: HTTPS (assumed, not enforced in code) - Secrets management: Environment variables (`ASPNETCORE_*` prefix) **Audit logging**: No explicit audit trail. Serilog logs business exceptions (WARN) and general events (INFO). ## 8. Key Architectural Decisions ### ADR-001: Minimal API over Controllers **Context**: API has ~17 endpoints with simple request/response patterns. **Decision**: Use ASP.NET Core Minimal API with top-level statements instead of MVC controllers. **Consequences**: All endpoints in a single `Program.cs`. Simple for small APIs but could become unwieldy as endpoints grow. ### ADR-002: Read/Write Database Connection Separation **Context**: Needed different privilege levels for read vs. write operations. **Decision**: `DbFactory` maintains two connection strings — a read-only one (`AzaionDb`) and an admin one (`AzaionDbAdmin`) — with separate `Run` and `RunAdmin` methods. **Consequences**: Write operations are explicitly gated through `RunAdmin`. Prevents accidental writes through the reader connection. Requires maintaining two DB users with different privileges. ### ADR-003: Per-User Resource Encryption — RETIRED (cycle 2, 2026-05-14) **Original context**: Resources (DLLs, AI models) had to be delivered only to authorized users via a per-download AES-256-CBC stream keyed off the user's email + password. **Retirement decision**: With the OTA delivery flow (AZ-183) and the hardware-binding flow (AZ-197) both gone, the only remaining consumer of the encrypted-download path was a now-vestigial `POST /resources/get/{dataFolder?}` endpoint and the two installer endpoints. None of them are part of the target architecture (browser SaaS + fTPM Jetsons), so the entire encrypt-on-download stack — `POST /resources/get`, `GET /resources/get-installer`, `GET /resources/get-installer/stage`, `ResourcesService.GetEncryptedResource`, `ResourcesService.GetInstaller`, `Security.GetApiEncryptionKey`, `Security.EncryptTo`, `Security.DecryptTo`, `GetResourceRequest`, `WrongResourceName` (50), `ResourcesConfig.SuiteInstallerFolder` / `SuiteStageInstallerFolder` — was removed. `Security.ToHash` is retained because it still backs SHA-384 password hashing in `UserService`. **Consequences**: resource files now live on disk as plain bytes; any future at-rest encryption must come from filesystem or storage-layer features (LUKS, object-store SSE), not from application code. ### ADR-004: Hardware Fingerprint Binding — RETIRED (AZ-197) **Original context**: Resources should only be usable on a specific physical machine. **Original decision**: On first resource access, the user's hardware fingerprint string was stored. Subsequent accesses compared the hash of the provided hardware against the stored value. **Retirement decision (2026-05-13, AZ-197)**: The threat model that motivated this binding (credential reuse across machines via desktop installers) no longer applies: - **Edge devices** ship as **fTPM-secured Jetsons** (secure boot, fTPM-protected key storage, no user filesystem access, no installer redistribution). Hardware identity is anchored in the fTPM, not in a SHA-384 of CPU/GPU/Memory/DriveSerial strings. - **Server / desktop access** is **SaaS-only** (browser → admin API). There is no installer to copy and no hardware fingerprint to take. The binding's only remaining effect was a real production failure mode (`HardwareIdMismatch`, error code 40) on legitimate hardware events. AZ-197 removed `CheckHardwareHash`, `UpdateHardware`, `Security.GetHWHash`, the `PUT /users/hardware/set` and `POST /resources/check` endpoints, and the `Hardware` field from `GetResourceRequest`. The `User.Hardware` DB column is a nullable tombstone (no migration in AZ-197; separate ticket if/when the column is dropped). ### ADR-005: linq2db over Entity Framework **Context**: Needed a lightweight ORM for PostgreSQL. **Decision**: Use linq2db instead of Entity Framework Core. **Consequences**: No migration framework — schema managed via SQL scripts (`env/db/`). Lighter runtime footprint. Manual mapping configuration in `AzaionDbSchemaHolder`.