- Deleted the deploy.cmd script as it was no longer needed. - Updated Dockerfile to include curl for health checks and added a non-root user for improved security. - Modified health check command to use curl for better reliability. - Adjusted docker-compose.test.yml to reflect changes in health check configuration. - Cleaned up appsettings.json and removed unused configuration properties. - Removed Resource entity and related requests from the codebase as part of the architectural shift. - Updated documentation to reflect the removal of hardware binding and related endpoints. Co-authored-by: Cursor <cursoragent@cursor.com>
10 KiB
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 with per-user AES encryption.
- 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/BadHardwareerror codes) was removed. Edge devices now ship as fTPM-secured Jetsons; server/desktop access is SaaS-only. TheUser.HardwareDB column remains as a nullable tombstone (no migration in AZ-197).
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
Resourceentity, theresourcestable, 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/download
- Client → API → Security → ResourcesService: encrypted resource retrieval (key derived from user email + password; hardware-hash component removed in AZ-197)
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 |
| File encryption | AES-256-CBC | Per-resource | High |
| Password hashing | SHA-384 | Per-user | Medium |
| Cache TTL | 4 hours | User entity cache | Low |
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— requiresApiAdminrole- General
[Authorize]— any authenticated user
The
apiUploaderPolicywas added by AZ-183 and removed in the post-cycle-1 revert along with the OTA endpoints it guarded.RoleEnum.ResourceUploaderremains as data only.
Data protection:
- At rest: Resources encrypted with AES-256-CBC using per-user derived key (email + password). The hardware-hash component was removed in AZ-197 (sealed-Jetson + SaaS architecture).
- 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
Context: Resources (DLLs, AI models) must be delivered only to authorized users.
Decision: Resources are encrypted at download time using AES-256-CBC with a key derived from the user's email and password. The client must know both to decrypt.
Consequences: Strong per-user binding. However, encryption happens in memory (MemoryStream), which limits practical file sizes. Key derivation is deterministic — same inputs always produce the same key.
Update (AZ-197, 2026-05-13): the hardware-hash component of the derivation was removed. The new key formula is
SHA384(email + "-" + password + "-#%@AzaionKey@%#---"). See ADR-004 for context on why the hardware binding was retired.
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.