refactor: remove obsolete resource download and installer endpoints
ci/woodpecker/push/01-test Pipeline failed
ci/woodpecker/push/02-build-push unknown status

- Deleted the `POST /resources/get/{dataFolder?}` and `GET /resources/get-installer` endpoints as part of the architectural shift towards simplified resource management.
- Removed associated methods and configurations, including `ResourcesService.GetEncryptedResource`, `ResourcesService.GetInstaller`, and related properties in `ResourcesConfig`.
- Cleaned up environment variables and configuration files to reflect the removal of installer-related settings.
- Eliminated the `GetResourceRequest` DTO and its validator, along with the `WrongResourceName` error code.
- Updated documentation to clarify the changes in resource handling and the retirement of per-user file encryption.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-14 04:17:55 +03:00
parent c7b297de83
commit 3a925b9b0f
60 changed files with 1202 additions and 982 deletions
+11 -11
View File
@@ -5,11 +5,13 @@
**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.
- **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 |
@@ -76,8 +78,7 @@
**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)
- Client → API → ResourcesService → Filesystem: resource upload / list / clear (encrypted download + installer delivery were retired in cycle 2)
## 5. Integration Points
@@ -103,10 +104,11 @@
| 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 |
> 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
@@ -120,7 +122,7 @@ No explicit availability, latency, throughput, or recovery targets found in the
> 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: 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).
- 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)
@@ -144,15 +146,13 @@ No explicit availability, latency, throughput, or recovery targets found in the
**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
### ADR-003: Per-User Resource Encryption — RETIRED (cycle 2, 2026-05-14)
**Context**: Resources (DLLs, AI models) must be delivered only to authorized users.
**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.
**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.
**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**: 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.
**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)
@@ -5,7 +5,8 @@ Shared utility extensions used across multiple components.
## Modules
- `EnumExtensions` — enum description/attribute extraction (used by BusinessException)
- `StringExtensions` — PascalCase → snake_case conversion (used by AzaionDbSchemaHolder)
- `StreamExtensions` — Stream → string conversion (used by SecurityTest)
<!-- StreamExtensions removed in cycle 2 (2026-05-14): only consumer was the deleted SecurityTest. -->
- `QueryableExtensions` — conditional LINQ Where filter (used by UserService)
## Consumers
@@ -8,14 +8,16 @@ Domain exception type with catalog of business error codes (`ExceptionEnum`).
| NoEmailFound | 10 | No such email found |
| EmailExists | 20 | Email already exists |
| WrongPassword | 30 | Passwords do not match |
| PasswordLengthIncorrect | 32 | Password should be at least 8 characters |
| PasswordLengthIncorrect | 32 | Password should be at least 12 characters (validator threshold is 12 in `RegisterUserValidator`; the description text on the enum still reads "12 characters") |
| EmailLengthIncorrect | 35 | Email is empty or invalid |
| WrongEmail | 37 | (no description) |
| HardwareIdMismatch | 40 | Hardware mismatch |
| BadHardware | 45 | Hardware should be not empty |
| WrongResourceName | 50 | Wrong resource file name |
| UserDisabled | 38 | User account is disabled |
| NoFileProvided | 60 | No file provided |
> **Retired numeric codes — DO NOT REUSE**:
> - `40` (HardwareIdMismatch) and `45` (BadHardware) — removed by AZ-197 (cycle 1, 2026-05-13). Older clients may still surface "Hardware mismatch" UX strings keyed on these integers.
> - `50` (WrongResourceName) — removed in cycle 2 (2026-05-14) along with the `GetResourceRequest` validator (its only consumer) and the `POST /resources/get/{dataFolder?}` endpoint.
## Consumers
| Component | Usage |
|-----------|-------|
@@ -30,6 +30,8 @@
### Entities
> **Cycle 1 (2026-05-13) note** — `DetectionClass` (AZ-513) entity was added. `Resource` (AZ-183) was added then removed in the same cycle (post-cycle-1 revert; security audit F-1 + the OTA delivery model itself was deemed obsolete). The `User.Hardware` column is left in place as a tombstone (nullable, unused) per AZ-197. A UNIQUE INDEX `users_email_uidx` was added on `users.email` (security audit F-3, `env/db/06_users_email_unique.sql`).
>
> **Cycle 2 (2026-05-14) note** — `ResourcesConfig.SuiteInstallerFolder` and `SuiteStageInstallerFolder` were removed along with the installer endpoints (`GET /resources/get-installer[/stage]`); the POCO is now a single-property class (`ResourcesFolder`).
```
User:
@@ -81,8 +83,7 @@ JwtConfig:
ResourcesConfig:
ResourcesFolder: string
SuiteInstallerFolder: string
SuiteStageInstallerFolder: string
# SuiteInstallerFolder / SuiteStageInstallerFolder removed in cycle 2 with the installer endpoints.
# EncryptionMasterKey was added by AZ-183 and removed in the post-cycle-1 revert.
```
@@ -1,16 +1,18 @@
# Authentication & Security
> **Cycle 1 (2026-05-13) note** — AZ-197 simplified `GetApiEncryptionKey` to `(email, password)` and removed `GetHWHash` outright. The hardware-binding threat model that motivated those primitives is no longer in scope (fTPM-anchored Jetsons + browser SaaS).
>
> **Cycle 2 (2026-05-14) note** — `GetApiEncryptionKey`, `EncryptTo`, and `DecryptTo` were all removed along with the encrypted-download endpoint. `Security` is now a one-method utility (`ToHash`) that backs SHA-384 password hashing.
## 1. High-Level Overview
**Purpose**: JWT token creation/validation and cryptographic utilities (password hashing, AES file encryption/decryption).
**Purpose**: JWT token creation/validation and password hashing (`Security.ToHash`).
**Architectural Pattern**: Service + static utility — `AuthService` is a DI-managed service for JWT operations; `Security` is a static class for cryptographic primitives.
**Architectural Pattern**: Service + static utility — `AuthService` is a DI-managed service for JWT operations; `Security` is a static class with a single SHA-384 helper.
**Upstream dependencies**: Data Layer (JwtConfig, IUserService for GetByEmail), ASP.NET Core (IHttpContextAccessor).
**Downstream consumers**: Admin API (token creation on login, current user resolution), User Management (password hashing for both web users and provisioned devices), Resource Management (encryption key derivation, stream encryption).
**Downstream consumers**: Admin API (token creation on login, current user resolution), User Management (password hashing for both web users and provisioned devices).
## 2. Internal Interfaces
@@ -26,11 +28,11 @@
| Method | Input | Output | Description |
|--------|-------|--------|-------------|
| `ToHash` | `string` | `string` (Base64) | SHA-384 hash |
| `GetApiEncryptionKey` | `string email, string password` | `string` (Base64) | Derives the per-user AES encryption key string. **Signature simplified by AZ-197** (`hardwareHash` parameter removed). |
| `EncryptTo` | `Stream input, Stream output, string key, CancellationToken` | void | AES-256-CBC encrypt stream |
| `DecryptTo` | `Stream encrypted, Stream output, string key, CancellationToken` | void | AES-256-CBC decrypt stream |
**Removed by AZ-197**: `GetHWHash(string hardware)` — no remaining callers in the post-cycle-1 codebase.
**Removed**:
- `GetHWHash(string hardware)` — removed by AZ-197 (cycle 1).
- `GetApiEncryptionKey(string email, string password)` — removed in cycle 2 (no remaining callers after `POST /resources/get/{dataFolder?}` was deleted).
- `EncryptTo` / `DecryptTo` extension methods — removed in cycle 2 (no remaining callers; the only consumer was `ResourcesService.GetEncryptedResource`, also deleted).
## 3. External API Specification
@@ -42,7 +44,7 @@ No direct database access. `AuthService.GetCurrentUser` delegates to `IUserServi
## 5. Implementation Details
**Algorithmic Complexity**: Encryption/decryption is O(n) where n is file size, streaming in 512 KB buffers.
**Algorithmic Complexity**: SHA-384 hashing is O(n) where n is input length; in practice it operates on short password strings only.
**State Management**: `AuthService` is stateless (reads claims from HTTP context per request). `Security` is purely static.
@@ -54,7 +56,6 @@ No direct database access. `AuthService.GetCurrentUser` delegates to `IUserServi
| Microsoft.AspNetCore.Authentication.JwtBearer | 10.0.3 | JWT middleware integration |
**Error Handling Strategy**:
- `EncryptTo` throws `ArgumentNullException` for unreadable streams or empty keys.
- JWT token creation does not throw (malformed config would cause runtime errors at middleware level).
- `GetCurrentUser` returns null if claims are missing or user not found.
@@ -65,15 +66,12 @@ None — `Security` itself is a utility consumed by other components.
## 7. Caveats & Edge Cases
**Known limitations**:
- Password hashing uses SHA-384 without per-user salt or key stretching. Not resistant to rainbow table attacks. (Unchanged by cycle 1.)
- The encryption-key salt is a hardcoded constant. (`Security.GetApiEncryptionKey` body — see `services_security.md`.)
- Password hashing uses SHA-384 without per-user salt or key stretching. Not resistant to rainbow table attacks. (Unchanged by cycles 1 and 2.)
- `GetCurrentUserEmail` assumes `ClaimTypes.Name` is always present; accessing a missing key would throw `KeyNotFoundException`.
- AES encryption prepends IV as first 16 bytes — consumers must know this format.
**Removed in cycle 1**: hardware fingerprint hashing was a known weakness (static salt, no rotation); deleting it via AZ-197 also removed that attack surface.
**Performance bottlenecks**:
- Large file encryption loads encrypted output into `MemoryStream` before sending — high memory usage for large files.
**Removed in cycle 2**: per-user file encryption (`GetApiEncryptionKey` + `EncryptTo` + `DecryptTo`). The hardcoded encryption-key salt and the in-memory `MemoryStream` round-trip are no longer attack / performance surfaces in this codebase.
## 8. Dependency Graph
@@ -81,7 +79,7 @@ None — `Security` itself is a utility consumed by other components.
**Can be implemented in parallel with**: User Management (shared dependency on Data Layer).
**Blocks**: Admin API, Resource Management (uses encryption).
**Blocks**: Admin API. (Resource Management no longer depends on this component after cycle 2 removed `EncryptTo` / `DecryptTo`.)
## 9. Logging Strategy
@@ -1,14 +1,16 @@
# Resource Management
> **Cycle 1 (2026-05-13) note** — AZ-197 removed the `Hardware` field from `GetResourceRequest` and removed `CheckResourceRequest` and `POST /resources/check` entirely. AZ-183 introduced an OTA update path (`POST /get-update`, `POST /resources/publish`, `IResourceUpdateService`, `Resource` entity, `resources` table, `ResourcesConfig.EncryptionMasterKey`) but it was reverted later the same day after the security audit (finding F-1) — the OTA delivery model itself was deemed obsolete. The component is now back to filesystem-backed storage only.
> **Cycle 1 (2026-05-13) note** — AZ-197 removed the `Hardware` field from `GetResourceRequest` and removed `CheckResourceRequest` and `POST /resources/check` entirely. AZ-183 introduced an OTA update path (`POST /get-update`, `POST /resources/publish`, `IResourceUpdateService`, `Resource` entity, `resources` table, `ResourcesConfig.EncryptionMasterKey`) but it was reverted later the same day after the security audit (finding F-1) — the OTA delivery model itself was deemed obsolete.
>
> **Cycle 2 (2026-05-14) note** — the encrypted-download endpoint (`POST /resources/get/{dataFolder?}`) and both installer endpoints (`GET /resources/get-installer[/stage]`) were removed as obsolete. With them went `ResourcesService.GetEncryptedResource` / `GetInstaller`, `GetResourceRequest` (and `WrongResourceName = 50`), and the `ResourcesConfig.SuiteInstallerFolder` / `SuiteStageInstallerFolder` properties + their env-var rows. The component is now upload + list + clear only and no longer depends on Authentication & Security for encryption primitives.
## 1. High-Level Overview
**Purpose**: filesystem-backed storage — upload, list, download (per-user AES-encrypted), folder clearing, installer distribution. Owned by `IResourcesService`.
**Purpose**: filesystem-backed storage — upload, list, clear. Owned by `IResourcesService`.
**Architectural Pattern**: a single service over the local filesystem. No DB access, no cache.
**Upstream dependencies**: Data Layer (`ResourcesConfig`), Authentication & Security (encryption via `Security.EncryptTo`).
**Upstream dependencies**: Data Layer (`ResourcesConfig`).
**Downstream consumers**: Admin API (resource endpoints).
@@ -18,22 +20,18 @@
| Method | Input | Output | Async | Error Types |
|--------|-------|--------|-------|-------------|
| `GetInstaller` | `bool isStage` | `(string?, Stream?)` | No | None (returns nulls if not found) |
| `GetEncryptedResource` | `string? dataFolder, string fileName, string key, CancellationToken` | `Stream` | Yes | `FileNotFoundException` |
| `SaveResource` | `string? dataFolder, IFormFile data, CancellationToken` | void | Yes | `BusinessException(NoFileProvided)` |
| `ListResources` | `string? dataFolder, string? search, CancellationToken` | `IEnumerable<string>` | Yes | `DirectoryNotFoundException` |
| `ClearFolder` | `string? dataFolder` | void | No | None |
**Input DTO**:
```
GetResourceRequest (post-AZ-197):
Password: string (required, min 8 chars)
FileName: string (required, not empty)
// Hardware field removed by AZ-197.
**Removed**:
- `GetEncryptedResource` — removed in cycle 2 with the encrypted-download endpoint.
- `GetInstaller` — removed in cycle 2 with the installer endpoints.
// CheckResourceRequest — REMOVED by AZ-197.
// GetUpdateRequest, PublishResourceRequest — added by AZ-183, removed in the post-cycle-1 revert.
```
**Removed DTOs**:
- `GetResourceRequest`removed in cycle 2 (file deleted).
- `CheckResourceRequest` — removed by AZ-197 (cycle 1).
- `GetUpdateRequest`, `PublishResourceRequest` — removed in the post-cycle-1 AZ-183 revert.
## 3. External API Specification
@@ -49,7 +47,7 @@ N/A — exposed through Admin API.
### Storage Estimates
- **Filesystem**: AI models, DLLs, installers — potentially hundreds of MB per file.
- **Filesystem**: AI models, DLLs, etc. — potentially hundreds of MB per file.
## 5. Implementation Details
@@ -61,32 +59,26 @@ N/A — exposed through Admin API.
- `SaveResource` throws `BusinessException(NoFileProvided)` for null uploads.
- Missing files/directories throw standard .NET I/O exceptions.
- `ClearFolder` silently returns if directory doesn't exist.
- `GetInstaller` returns `(null, null)` tuple if installer file is not found.
## 6. Extensions and Helpers
| Helper | Purpose | Used By |
|--------|---------|---------|
| `Security.EncryptTo` | AES stream encryption | `GetEncryptedResource` |
| `Security.GetApiEncryptionKey(email, password)` | Per-user key derivation (post-AZ-197 — no hardware component) | Admin API (before calling `GetEncryptedResource`) |
None remaining after the cycle-2 removal of `Security.EncryptTo` and `Security.GetApiEncryptionKey`.
## 7. Caveats & Edge Cases
**Known limitations** (security-audit findings):
- **F-2 (High)** — no path traversal protection: `dataFolder` parameter is concatenated directly with `ResourcesFolder`. A malicious `dataFolder` like `../../etc` could access arbitrary filesystem paths. Filed as separate ticket.
- `SaveResource` deletes existing file before writing — no versioning or backup.
- `GetEncryptedResource` loads the entire encrypted file into a `MemoryStream` — memory-intensive for large files.
- `ListResources` wraps a synchronous `DirectoryInfo.GetFiles` in `Task.FromResult` — not truly async.
**Performance bottlenecks**:
- Full file encryption to memory before streaming response: memory usage scales with file size.
- `ClearFolder` iterates and deletes files synchronously.
## 8. Dependency Graph
**Must be implemented after**: Data Layer (ResourcesConfig), Authentication & Security (encryption).
**Must be implemented after**: Data Layer (ResourcesConfig).
**Can be implemented in parallel with**: User Management.
**Can be implemented in parallel with**: User Management, Authentication & Security.
**Blocks**: Admin API.
@@ -101,6 +93,5 @@ N/A — exposed through Admin API.
**Log storage**: console + rolling file (via Serilog configured in Program.cs).
## Modules Covered
- `Services/ResourcesService`
- `Common/Requests/GetResourceRequest` (post-AZ-197 — no `CheckResourceRequest`, no `Hardware` field)
- `Common/Configs/ResourcesConfig` (the `EncryptionMasterKey` field added by AZ-183 was removed in the post-cycle-1 revert)
- `Services/ResourcesService` (post-cycle-2 — only `SaveResource` / `ListResources` / `ClearFolder` remain)
- `Common/Configs/ResourcesConfig` (post-cycle-2 — only `ResourcesFolder` remains)
@@ -50,12 +50,10 @@ Converts `BusinessException` to HTTP 409 JSON response: `{ ErrorCode: int, Messa
| `/resources/{dataFolder?}` | POST | Authenticated | Uploads a file (up to 200 MB) |
| `/resources/list/{dataFolder?}` | GET | Authenticated | Lists files |
| `/resources/clear/{dataFolder?}` | POST | ApiAdmin | Clears folder |
| `/resources/get/{dataFolder?}` | POST | Authenticated | Downloads encrypted resource (key derived from `email + password` only — no Hardware) |
| `/resources/get-installer` | GET | Authenticated | Downloads production installer |
| `/resources/get-installer/stage` | GET | Authenticated | Downloads staging installer |
**Removed by AZ-197**: `POST /resources/check` (was the hardware-binding side-effect probe).
**Removed in post-cycle-1 revert**: `POST /get-update` and `POST /resources/publish` (AZ-183 reverted — security audit F-1; OTA delivery model itself obsolete).
**Removed in cycle 2 (2026-05-14)**: `POST /resources/get/{dataFolder?}`, `GET /resources/get-installer`, `GET /resources/get-installer/stage` — all obsolete; the encrypted-download support stack (`Security.GetApiEncryptionKey` / `EncryptTo` / `DecryptTo`, `ResourcesService.GetEncryptedResource` / `GetInstaller`, `GetResourceRequest`, `WrongResourceName = 50`, `ResourcesConfig.SuiteInstallerFolder` / `SuiteStageInstallerFolder`) was removed with them. ADR-003 retired.
### Detection Classes
| Endpoint | Method | Auth | Description |
@@ -10,7 +10,7 @@
## Configuration
### appsettings.json Defaults
- `ResourcesConfig`: ResourcesFolder=`"Content"`, SuiteInstallerFolder=`"suite"`, SuiteStageInstallerFolder=`"suite-stage"`
- `ResourcesConfig`: ResourcesFolder=`"Content"` (the `SuiteInstallerFolder` / `SuiteStageInstallerFolder` keys were removed in cycle 2 along with the installer endpoints)
- `JwtConfig`: Issuer=`"AzaionApi"`, Audience=`"Annotators/OrangePi/Admins"`, TokenLifetimeHours=`4`
- `ConnectionStrings` and `JwtConfig.Secret` are NOT in appsettings — must be provided via environment variables
@@ -25,8 +25,6 @@ Configuration is loaded via ASP.NET Core's `IConfiguration` with the following s
| `JwtConfig.Audience` | Token audience | — |
| `JwtConfig.TokenLifetimeHours` | Token TTL | — |
| `ResourcesConfig.ResourcesFolder` | File storage root | — |
| `ResourcesConfig.SuiteInstallerFolder` | Prod installer dir | — |
| `ResourcesConfig.SuiteStageInstallerFolder` | Stage installer dir | — |
## Infrastructure Scripts (`env/`)
+4 -5
View File
@@ -65,11 +65,10 @@ graph TD
| # | Component | Modules | Purpose |
|---|-----------|---------|---------|
| 01 | Data Layer | 9 | DB access, entities, configs, caching |
| 02 | User Management | 5 | User CRUD, hardware binding, role management |
| 03 | Auth & Security | 2 | JWT tokens, cryptographic utilities |
| 04 | Resource Management | 3 | File upload/download/encryption |
| 02 | User Management | 5 | User CRUD, role management, device provisioning (hardware binding removed by AZ-197) |
| 03 | Auth & Security | 2 | JWT tokens + SHA-384 password hashing (per-user file encryption removed in cycle 2) |
| 04 | Resource Management | 2 | File upload / list / clear (encrypted-download + installer endpoints removed in cycle 2) |
| 05 | Admin API | 2 | HTTP endpoints, middleware, DI composition |
| — | Common Helpers | 6 | Extensions, BusinessException |
| — | Tests | 2 | SecurityTest, UserServiceTest |
**Total**: 27 modules across 5 components + common helpers + tests.
**Total**: 26 modules across 5 components + common helpers. The previously listed in-process unit tests (`SecurityTest`, `UserServiceTest`) and the `Azaion.Test` project itself were removed in cycle 2; remaining test coverage lives in `e2e/Azaion.E2E/`.
@@ -1,29 +1,14 @@
# Flow: Encrypted Resource Download
# Flow: Encrypted Resource Download — OBSOLETE
> **Removed in cycle 2 (2026-05-14).**
>
> The `POST /resources/get/{dataFolder?}` endpoint, the `ResourcesService.GetEncryptedResource` method, the `Security.GetApiEncryptionKey` / `EncryptTo` / `DecryptTo` helpers, the `GetResourceRequest` DTO + validator, and the `ExceptionEnum.WrongResourceName` (50) error code no longer exist. Per-user file encryption is no longer part of the system; resource files are stored as plain bytes and only ever leave the server through upload (`POST /resources/{dataFolder?}`) and admin clear (`POST /resources/clear/{dataFolder?}`).
>
> See `_docs/02_document/architecture.md` ADR-003 (retired) and `_docs/02_document/system-flows.md` flow F3 (removed) for context.
>
> This file is retained as a tombstone so historical references resolve. Do not link to it from new docs.
```mermaid
sequenceDiagram
participant Client
participant API as Admin API
participant Auth as AuthService
participant US as UserService
participant Sec as Security
participant RS as ResourcesService
participant FS as Filesystem
Client->>API: POST /resources/get {password, hardware, fileName}
API->>Auth: GetCurrentUser()
Auth-->>API: User
API->>US: CheckHardwareHash(user, hardware)
US->>Sec: GetHWHash(hardware)
Sec-->>US: hash
US-->>API: hwHash
API->>Sec: GetApiEncryptionKey(email, password, hwHash)
Sec-->>API: AES key
API->>RS: GetEncryptedResource(folder, fileName, key)
RS->>FS: Read file
FS-->>RS: FileStream
RS->>Sec: EncryptTo(stream, key) [AES-256-CBC]
Sec-->>RS: Encrypted MemoryStream
RS-->>API: Stream
API-->>Client: 200 OK (application/octet-stream)
flowchart TD
Start([POST /resources/get — REMOVED]) --> Removed[Endpoint deleted in cycle 2]
```
+5 -8
View File
@@ -7,11 +7,11 @@
## Layout Rules
1. This admin/ workspace is one **deployable** (the `Azaion.AdminApi` HTTP service) split across four production csproj projects + one e2e test csproj: `Azaion.AdminApi`, `Azaion.Services`, `Azaion.Common`, `Azaion.Test`, `e2e/Azaion.E2E`.
1. This admin/ workspace is one **deployable** (the `Azaion.AdminApi` HTTP service) split across three production csproj projects + one e2e test csproj: `Azaion.AdminApi`, `Azaion.Services`, `Azaion.Common`, `e2e/Azaion.E2E`. (The `Azaion.Test` unit-test project was removed in cycle 2 once its only test class — `SecurityTest.cs` — was deleted along with the encrypted-download stack; no in-process unit tests remain.)
2. Existing task specs (`_docs/02_tasks/*/AZ-*.md`) all use `Component: Admin API` as a single coarse identifier covering this entire workspace. The Per-Component Mapping below honors that convention rather than rewriting every task spec.
3. The conceptual sub-components documented in `_docs/02_document/components/01_data_layer..05_admin_api/` are **read-time** documentation aids, not write-time ownership boundaries. They are listed under "Conceptual Sub-Components" below for reference only.
4. Public API surface = the namespaces / interfaces exposed across csproj boundaries (`I*Service` interfaces in `Azaion.Services`, request DTOs in `Azaion.Common/Requests/`, entities in `Azaion.Common/Entities/`).
5. Tests live in `Azaion.Test/` (in-process unit/integration) and `e2e/Azaion.E2E/` (HTTP black-box). Production code never imports from either.
5. Tests live in `e2e/Azaion.E2E/` (HTTP black-box). Production code never imports from there.
## Per-Component Mapping
@@ -23,10 +23,8 @@
- `Azaion.AdminApi/**`
- `Azaion.Services/**`
- `Azaion.Common/**`
- `Azaion.Test/**`
- `e2e/Azaion.E2E/**` (xUnit/HttpClient-based black-box tests)
- `e2e/db-init/**` (test-DB seed/init scripts consumed by the e2e harness)
- `docker.test/**` (test fixture / schema-init helpers used by `Azaion.Test`)
- `docker-compose.test.yml`
- **Public API** (visible to other csprojs within the workspace):
- `Azaion.Services/I*Service.cs` interfaces (UserService, AuthService, ResourcesService, …)
@@ -54,8 +52,8 @@ These come from `_docs/02_document/components/` and exist for reading the codeba
|---|----------------------|------------------------|
| 1 | Data Layer | `Azaion.Common/Database/`, `Azaion.Common/Configs/`, `Azaion.Common/Entities/` (incl. `DetectionClass.cs` added cycle 1; `Resource.cs` added then removed in same cycle — see post-cycle-1 revert) |
| 2 | User Management | `Azaion.Services/UserService.cs` (incl. `RegisterDevice` added cycle 1 / AZ-196 — calls `RegisterUser` end-to-end after security-audit consolidation, finding F-3), `Azaion.Common/Requests/Register{User,DeviceResponse}.cs`, `LoginRequest.cs`, `SetUserQueueOffsetsRequest.cs` |
| 3 | Auth & Security | `Azaion.Services/AuthService.cs`, `Azaion.Services/Security.cs` (post-AZ-197 — `GetHWHash` removed; signature simplified), `Azaion.Services/Cache.cs` |
| 4 | Resource Management | `Azaion.Services/ResourcesService.cs`, `Azaion.Common/Requests/GetResourceRequest.cs` (`SetHWRequest.cs` removed by AZ-197; `ResourceUpdateService.cs` + `GetUpdateRequest.cs` + `PublishResourceRequest.cs` removed when AZ-183 was reverted) |
| 3 | Auth & Security | `Azaion.Services/AuthService.cs`, `Azaion.Services/Security.cs` (post-cycle-2 — only `ToHash` remains; `GetApiEncryptionKey` / `EncryptTo` / `DecryptTo` removed with the encrypted-download endpoint), `Azaion.Services/Cache.cs` |
| 4 | Resource Management | `Azaion.Services/ResourcesService.cs` (`GetResourceRequest.cs` removed in cycle 2 with `POST /resources/get`; `SetHWRequest.cs` removed by AZ-197; `ResourceUpdateService.cs` + `GetUpdateRequest.cs` + `PublishResourceRequest.cs` removed when AZ-183 was reverted) |
| 4b | Detection Classes | `Azaion.Services/DetectionClassService.cs` + `Azaion.Common/Requests/{Create,Update}DetectionClassRequest.cs` (added cycle 1 / AZ-513) |
| 5 | Admin API (HTTP) | `Azaion.AdminApi/Program.cs`, `Azaion.AdminApi/BusinessExceptionHandler.cs`, `Azaion.AdminApi/appsettings*.json` |
@@ -66,7 +64,6 @@ These come from `_docs/02_document/components/` and exist for reading the codeba
| 4. Entry / Host | `Azaion.AdminApi` | `Azaion.Services`, `Azaion.Common` |
| 3. Application | `Azaion.Services` | `Azaion.Common` |
| 2. Foundation | `Azaion.Common` | (none) |
| —. Tests (in-process) | `Azaion.Test` | `Azaion.Services`, `Azaion.Common`, `Azaion.AdminApi` (integration only) |
| —. Tests (out-of-process e2e) | `e2e/Azaion.E2E` | (none from production csprojs — HTTP only) |
A reference from a lower production layer to a higher production layer is an **Architecture** finding (High severity) in `/code-review` Phase 7. Test projects may reference any production csproj; production csprojs may NOT reference test projects.
@@ -75,7 +72,7 @@ A reference from a lower production layer to a higher production layer is an **A
| Language | Root | Per-component path | Public API file | Test path |
|----------|------|-------------------|-----------------|-----------|
| C# (.NET) | `./` (this workspace, legacy flat layout) | `./<Csproj>/` | namespace-root types in each csproj | `Azaion.Test/`, `e2e/Azaion.E2E/` |
| C# (.NET) | `./` (this workspace, legacy flat layout) | `./<Csproj>/` | namespace-root types in each csproj | `e2e/Azaion.E2E/` |
## Notes
+14 -12
View File
@@ -6,6 +6,8 @@ Application entry point: configures DI, middleware, authentication, authorizatio
## Public Interface (HTTP Endpoints)
> **Cycle 1 (2026-05-13) note** — endpoint surface changed by AZ-513 (detection-class CRUD), AZ-196 (device auto-registration), AZ-197 (hardware-binding removal). AZ-183 (OTA update check + publish) was reverted later the same day after the security audit (finding F-1) — the OTA delivery model itself was deemed obsolete; see `_docs/05_security/security_report.md` for context. The table reflects the post-cycle-1 state including that revert.
>
> **Cycle 2 (2026-05-14) note** — three more endpoints were removed as obsolete: `POST /resources/get/{dataFolder?}`, `GET /resources/get-installer`, `GET /resources/get-installer/stage`. The encrypted-download support stack (`Security.GetApiEncryptionKey` / `EncryptTo` / `DecryptTo`, `ResourcesService.GetEncryptedResource` / `GetInstaller`, `GetResourceRequest` DTO, `WrongResourceName = 50` enum value, `ResourcesConfig.SuiteInstallerFolder` / `SuiteStageInstallerFolder`) went with them. ADR-003 in `architecture.md` was retired in the same change.
| Method | Path | Auth | Summary | Cycle 1 origin |
|--------|------|------|---------|----------------|
@@ -22,23 +24,23 @@ Application entry point: configures DI, middleware, authentication, authorizatio
| POST | `/resources/{dataFolder?}` | Any authenticated | Uploads a resource file | — |
| GET | `/resources/list/{dataFolder?}` | Any authenticated | Lists files in a resource folder | — |
| POST | `/resources/clear/{dataFolder?}` | ApiAdmin | Clears a resource folder | — |
| POST | `/resources/get/{dataFolder?}` | Any authenticated | Downloads an encrypted resource (key derived from `email + password` only) | AZ-197 wire change (no `Hardware` field) |
| GET | `/resources/get-installer` | Any authenticated | Downloads latest production installer | — |
| GET | `/resources/get-installer/stage` | Any authenticated | Downloads latest staging installer | — |
| POST | `/classes` | ApiAdmin | Creates a detection class | AZ-513 |
| PATCH | `/classes/{id:int}` | ApiAdmin | Updates a detection class (partial-merge) | AZ-513 |
| DELETE | `/classes/{id:int}` | ApiAdmin | Deletes a detection class | AZ-513 |
### Removed in cycle 1
### Removed endpoints
The following endpoints were removed during cycle 1 and now return `404`:
The following endpoints have been removed and now return `404`:
| Method | Path | Reason removed |
|--------|------|----------------|
| PUT | `/users/hardware/set` | AZ-197 hardware-binding feature deleted (no fielded clients in target architecture) |
| POST | `/resources/check` | AZ-197 was the hardware-binding side-effect probe; no remaining purpose |
| POST | `/get-update` | OTA delivery model retired post-cycle-1 (security audit F-1: endpoint disclosed plaintext per-resource encryption keys to any authenticated caller; the underlying installer-distribution flow is itself obsolete) |
| POST | `/resources/publish` | Same revert as `/get-update` — the publish counterpart of the OTA flow |
| Method | Path | Removed in | Reason |
|--------|------|------------|--------|
| PUT | `/users/hardware/set` | cycle 1 (AZ-197) | hardware-binding feature deleted (no fielded clients in target architecture) |
| POST | `/resources/check` | cycle 1 (AZ-197) | was the hardware-binding side-effect probe; no remaining purpose |
| POST | `/get-update` | post-cycle-1 (AZ-183 reverted) | security audit F-1: endpoint disclosed plaintext per-resource encryption keys to any authenticated caller; the underlying installer-distribution flow is itself obsolete |
| POST | `/resources/publish` | post-cycle-1 (AZ-183 reverted) | same revert as `/get-update` — the publish counterpart of the OTA flow |
| POST | `/resources/get/{dataFolder?}` | cycle 2 (2026-05-14) | obsolete — per-user encrypted-download flow no longer used by any client; ADR-003 retired |
| GET | `/resources/get-installer` | cycle 2 (2026-05-14) | obsolete — installer-shipping era is over (browser SaaS + fTPM Jetsons) |
| GET | `/resources/get-installer/stage` | cycle 2 (2026-05-14) | same as `/resources/get-installer` |
## Internal Logic
@@ -69,7 +71,7 @@ The following endpoints were removed during cycle 1 and now return `404`:
### Configuration Sections
- `JwtConfig` — JWT signing/validation
- `ConnectionStrings` — DB connections
- `ResourcesConfig` — file storage paths
- `ResourcesConfig` — file storage path (`ResourcesFolder`); the installer subfolders were dropped in cycle 2 along with the installer endpoints
### Kestrel
- Max request body size: 200 MB (for file uploads)
@@ -22,10 +22,11 @@ Custom exception type for domain-level errors, paired with an `ExceptionEnum` ca
| `EmailLengthIncorrect` | 35 | Email is empty or invalid |
| `WrongEmail` | 37 | (no description attribute) |
| `UserDisabled` | 38 | User account is disabled |
| `WrongResourceName` | 50 | Wrong resource file name |
| `NoFileProvided` | 60 | No file provided |
> **Cycle 1 (2026-05-13) note** — `HardwareIdMismatch = 40` and `BadHardware = 45` were removed by AZ-197 (admin-side hardware-binding cleanup). Code 40 should NOT be reused for a different meaning — older clients may still surface "Hardware mismatch" UX strings keyed on the integer. `UserDisabled = 38` was added earlier (still part of the baseline). See `_docs/03_implementation/batch_06_report.md`.
> **Cycle 1 (2026-05-13) note** — `HardwareIdMismatch = 40` and `BadHardware = 45` were removed by AZ-197 (admin-side hardware-binding cleanup). Codes 40 and 45 should NOT be reused for a different meaning — older clients may still surface "Hardware mismatch" UX strings keyed on the integer. `UserDisabled = 38` was added earlier (still part of the baseline). See `_docs/03_implementation/batch_06_report.md`.
>
> **Cycle 2 (2026-05-14) note** — `WrongResourceName = 50` was removed along with the `GetResourceRequest` validator (the only consumer). Code 50 should NOT be reused — gap kept per the cycle-1 lesson on retired numeric codes.
## Internal Logic
Static constructor eagerly loads all `ExceptionEnum` descriptions into a dictionary via `EnumExtensions.GetDescriptions<ExceptionEnum>()`. Messages are retrieved by dictionary lookup with fallback to `ToString()`.
@@ -1,15 +1,15 @@
# Module: Azaion.Common.Configs.ResourcesConfig
## Purpose
Configuration POCO for file resource storage paths, bound from `appsettings.json` section `ResourcesConfig`.
Configuration POCO for the file resource storage root, bound from `appsettings.json` section `ResourcesConfig`.
> **Cycle 2 (2026-05-14) note** — `SuiteInstallerFolder` and `SuiteStageInstallerFolder` were removed along with the installer endpoints (`GET /resources/get-installer[/stage]`) and `ResourcesService.GetInstaller`. Their `ASPNETCORE_ResourcesConfig__SuiteInstallerFolder` / `__SuiteStageInstallerFolder` env-var rows were removed from `appsettings.json`, `.env.example`, `secrets/staging.public.env`, `secrets/production.public.env`, and `docker-compose.test.yml`.
## Public Interface
| Property | Type | Description |
|----------|------|-------------|
| `ResourcesFolder` | `string` | Root directory for uploaded resource files |
| `SuiteInstallerFolder` | `string` | Subdirectory for production installer files |
| `SuiteStageInstallerFolder` | `string` | Subdirectory for staging installer files |
## Internal Logic
None — pure data class.
@@ -18,7 +18,7 @@ None — pure data class.
None.
## Consumers
- `ResourcesService` — uses all three properties to resolve file paths
- `ResourcesService` — uses `ResourcesFolder` to resolve upload / list / clear paths
## Data Models
None.
@@ -30,7 +30,7 @@ Bound via `builder.Configuration.GetSection(nameof(ResourcesConfig))` in `Progra
None.
## Security
Paths control where files are read from and written to on the server's filesystem.
Path controls where files are read from and written to on the server's filesystem.
## Tests
None.
@@ -59,4 +59,4 @@ None.
`PasswordHash` stores SHA-384 hash. `Hardware` stores raw hardware fingerprint (hashed for comparison via `Security.GetHWHash`).
## Tests
Indirectly tested via `UserServiceTest` and `SecurityTest`.
Indirectly tested end-to-end via `e2e/Azaion.E2E/Tests/LoginTests.cs`, `UserManagementTests.cs`, and `DeviceTests.cs`. (The previous in-process `Azaion.Test/UserServiceTest` and `SecurityTest` were both removed by cycle 2 along with the `Azaion.Test` project.)
@@ -1,34 +0,0 @@
# Module: Azaion.Common.Extensions.StreamExtensions
## Purpose
Stream-to-string conversion utility.
## Public Interface
| Method | Signature | Description |
|--------|-----------|-------------|
| `ConvertToString` | `static string ConvertToString(this Stream stream)` | Reads entire stream as UTF-8 string, resets position to 0 afterward |
## Internal Logic
Resets stream position to 0, reads via `StreamReader`, then resets again so the stream remains usable.
## Dependencies
- `System.Text.Encoding`, `System.IO.StreamReader` (BCL only)
## Consumers
- `SecurityTest.EncryptDecryptTest` — converts decrypted stream to string for assertion
## Data Models
None.
## Configuration
None.
## External Integrations
None.
## Security
None.
## Tests
Indirectly tested via `SecurityTest.EncryptDecryptTest`.
@@ -1,46 +0,0 @@
# Module: Azaion.Common.Requests.GetResourceRequest
## Purpose
Request DTO and validator for the `POST /resources/get/{dataFolder?}` endpoint. The user's password is supplied per-request so the server can derive the per-user AES encryption key for the response stream.
> **Cycle 1 (2026-05-13) note** — the `Hardware` property and its `BadHardware` validator rule were removed by AZ-197 (admin-side hardware-binding cleanup). The wire-compat policy was "drop entirely" — any client still sending `Hardware` will not see it deserialized. The companion `CheckResourceRequest` was removed along with the `POST /resources/check` endpoint. See `_docs/03_implementation/batch_06_report.md`.
## Public Interface
### GetResourceRequest
| Property | Type | Description |
|----------|------|-------------|
| `Password` | `string` | User's password (used to derive the encryption key) |
| `FileName` | `string` | Resource file to retrieve |
### GetResourceRequestValidator
| Rule | Constraint | Error Code |
|------|-----------|------------|
| `Password` min length | >= 8 chars | `PasswordLengthIncorrect` |
| `FileName` not empty | Required | `WrongResourceName` |
## Internal Logic
Validator uses `BusinessException.GetMessage()` to derive user-facing error messages from `ExceptionEnum`.
## Dependencies
- `BusinessException`, `ExceptionEnum`
- FluentValidation
## Consumers
- `Program.cs` `POST /resources/get/{dataFolder?}` endpoint
## Data Models
None.
## Configuration
None.
## External Integrations
None.
## Security
- Password is sent in the POST body (not URL) to avoid logging in access logs.
- Per-user encryption key derivation now uses `email + password` only (see `services_security.md`).
## Tests
- `e2e/Azaion.E2E/Tests/ResourceTests.cs` (encrypted download / round-trip) — updated by AZ-197 to stop sending `Hardware`
@@ -27,7 +27,7 @@ Private method:
## Consumers
- `Program.cs` `/login` endpoint — calls `CreateToken` after successful validation
- `Program.cs` `/users/current`, `/resources/get`, `/resources/get-installer`, `/resources/check` — call `GetCurrentUser`
- `Program.cs` `/users/current` — calls `GetCurrentUser` (the previously listed `/resources/get`, `/resources/get-installer`, `/resources/check` consumers were removed in cycle 2 / by AZ-197 along with their endpoints)
## Data Models
None.
@@ -1,23 +1,21 @@
# Module: Azaion.Services.ResourcesService
## Purpose
File-based resource management: upload, list, download (encrypted), clear, and installer retrieval from the server's filesystem.
File-based resource management: upload, list, and clear files in the server's filesystem.
> **Cycle 2 (2026-05-14) note** — `GetInstaller` and `GetEncryptedResource` were removed along with the `POST /resources/get/{dataFolder?}` and `GET /resources/get-installer[/stage]` endpoints; the corresponding interface methods, the `Security.EncryptTo` dependency, and the `ResourcesConfig.SuiteInstallerFolder` / `SuiteStageInstallerFolder` properties went with them. The service is now upload + list + clear only.
## Public Interface
### IResourcesService
| Method | Signature | Description |
|--------|-----------|-------------|
| `GetInstaller` | `(string?, Stream?) GetInstaller(bool isStage)` | Returns the latest installer file (prod or stage) |
| `GetEncryptedResource` | `Task<Stream> GetEncryptedResource(string? dataFolder, string fileName, string key, CancellationToken ct)` | Reads a file and returns it AES-encrypted |
| `SaveResource` | `Task SaveResource(string? dataFolder, IFormFile data, CancellationToken ct)` | Saves an uploaded file to the resource folder |
| `ListResources` | `Task<IEnumerable<string>> ListResources(string? dataFolder, string? search, CancellationToken ct)` | Lists file names in a resource folder, optionally filtered |
| `ClearFolder` | `void ClearFolder(string? dataFolder)` | Deletes all files and subdirectories in the specified folder |
## Internal Logic
- **GetResourceFolder**: resolves the target directory. If `dataFolder` is null/empty, uses `ResourcesConfig.ResourcesFolder` directly; otherwise, appends it as a subdirectory.
- **GetInstaller**: scans the installer folder for files matching `"AzaionSuite.Iterative*"`, returns the first match as a `FileStream`.
- **GetEncryptedResource**: opens the file, encrypts via `Security.EncryptTo` extension into a `MemoryStream`, returns the encrypted stream.
- **SaveResource**: creates the folder if needed, deletes any existing file with the same name, then copies the uploaded file.
- **ListResources**: uses `DirectoryInfo.GetFiles` with optional search pattern.
- **ClearFolder**: iterates and deletes all files and subdirectories.
@@ -26,24 +24,22 @@ File-based resource management: upload, list, download (encrypted), clear, and i
- `IOptions<ResourcesConfig>` — folder paths
- `ILogger<ResourcesService>` — logs successful saves
- `BusinessException` — thrown for null file uploads
- `Security.EncryptTo` — stream encryption extension
## Consumers
- `Program.cs`all `/resources/*` endpoints
- `Program.cs``POST /resources/{dataFolder?}` (upload), `GET /resources/list/{dataFolder?}`, `POST /resources/clear/{dataFolder?}`
## Data Models
None.
## Configuration
Uses `ResourcesConfig` (ResourcesFolder, SuiteInstallerFolder, SuiteStageInstallerFolder).
Uses `ResourcesConfig.ResourcesFolder`.
## External Integrations
Local filesystem for resource storage.
## Security
- Resources are encrypted per-user using a key derived from `email + password` (the hardware-hash component was removed by AZ-197 — see `services_security.md`).
- File deletion overwrites existing files before writing new ones.
- No path traversal protection on `dataFolder` parameter.
- No path traversal protection on `dataFolder` parameter (security audit F-2 — open).
## Tests
None at the module level. End-to-end coverage lives in `e2e/Azaion.E2E/Tests/ResourceTests.cs` (encrypted download / round-trip / 200 MB upload limit) — updated by AZ-197 to stop sending the `Hardware` field.
End-to-end coverage in `e2e/Azaion.E2E/Tests/ResourceTests.cs` `File_upload_succeeds` and `Upload_without_file_is_rejected_with_400_or_409_and_60_on_conflict`.
+10 -21
View File
@@ -1,50 +1,39 @@
# Module: Azaion.Services.Security
## Purpose
Static utility class providing cryptographic operations: password hashing, encryption key derivation, and AES-CBC stream encryption/decryption.
Static utility class providing the SHA-384 password hashing helper used by `UserService`.
> **Cycle 1 (2026-05-13) note** — `GetHWHash` was deleted and `GetApiEncryptionKey` was simplified from `(email, password, hardwareHash)` to `(email, password)` by AZ-197 (admin-side hardware-binding cleanup). The hardware-hash component of the derived key is gone; existing ciphertexts produced under the old derivation are no longer re-derivable from the new signature. See `_docs/03_implementation/batch_06_report.md`.
> **Cycle 1 (2026-05-13) note** — `GetHWHash` was deleted and `GetApiEncryptionKey` was simplified from `(email, password, hardwareHash)` to `(email, password)` by AZ-197.
>
> **Cycle 2 (2026-05-14) note** — `GetApiEncryptionKey`, `EncryptTo`, and `DecryptTo` were all removed along with the encrypted-download endpoint. Only `ToHash` remains; it still backs SHA-384 password hashing in `UserService` (`PasswordHash = request.Password.ToHash()`). The `Azaion.Test/SecurityTest.cs` unit tests went with the removed methods, leaving the `Azaion.Test` project empty (also removed from the solution). See `_docs/06_metrics/retro_2026-05-14.md` once cycle 2's retro lands.
## Public Interface
| Method | Signature | Description |
|--------|-----------|-------------|
| `ToHash` | `static string ToHash(this string str)` | Extension: SHA-384 hash of input, returned as Base64 |
| `GetApiEncryptionKey` | `static string GetApiEncryptionKey(string email, string password)` | Derives the per-user AES encryption key string from email + password (+ static salt) |
| `EncryptTo` | `static async Task EncryptTo(this Stream inputStream, Stream toStream, string key, CancellationToken ct)` | AES-256-CBC encrypts a stream; prepends IV to output |
| `DecryptTo` | `static async Task DecryptTo(this Stream encryptedStream, Stream toStream, string key, CancellationToken ct)` | Reads IV prefix, then AES-256-CBC decrypts stream |
## Internal Logic
- **Password hashing**: `ToHash` uses SHA-384 with UTF-8 encoding, outputting Base64.
- **Encryption key derivation**: `GetApiEncryptionKey` concatenates email and password with the static salt `"-#%@AzaionKey@%#---"`, then hashes via `ToHash` (SHA-384, Base64).
- **Encryption**: AES-256-CBC with PKCS7 padding. Key is SHA-256 of the derived key string. IV is randomly generated and prepended to the output stream. Uses 512 KB buffer for streaming.
- **Decryption**: Reads the first 16 bytes as IV, then AES-256-CBC decrypts with PKCS7 padding.
- `ToHash` uses SHA-384 with UTF-8 encoding, outputting Base64.
## Dependencies
- `System.Security.Cryptography` (Aes, SHA256, SHA384)
- `System.Security.Cryptography` (SHA384)
- `System.Text.Encoding`
## Consumers
- `Program.cs` `/resources/get/{dataFolder}` endpoint — calls `GetApiEncryptionKey(user.Email, request.Password)`
- `ResourcesService.GetEncryptedResource` — uses `EncryptTo` extension
- `Azaion.Test/SecurityTest` — directly tests `EncryptTo` / `DecryptTo` round-trips (no longer tests hardware-hash derivation)
- `Azaion.Services/UserService.cs``RegisterUser` (password storage) and `ValidateUser` (login comparison) both call `request.Password.ToHash()`
## Data Models
None.
## Configuration
- `BUFFER_SIZE = 524288` (512 KB) — hardcoded streaming buffer size
None.
## External Integrations
None.
## Security
Core cryptographic module. Key observations:
- Passwords are hashed with SHA-384 (no per-user salt, no key stretching — not bcrypt/scrypt/argon2). This is unchanged by AZ-197.
- AES encryption uses SHA-256 of the derived key, with random IV per encryption.
- All salts/prefixes are hardcoded constants.
- Per AZ-197: device hardware fingerprints no longer participate in key derivation. The threat that hardware binding mitigated (credential reuse via desktop installers) was eliminated by the architectural shift to fTPM-secured Jetsons + browser-only SaaS access.
- Password hashing uses SHA-384 with no per-user salt and no key stretching. Not resistant to rainbow-table attacks (security audit F-7 — open). Unchanged by cycles 1 and 2.
## Tests
- `Azaion.Test/SecurityTest.EncryptDecryptTest` — round-trip encrypt/decrypt of a string
- `Azaion.Test/SecurityTest.EncryptDecryptLargeFileTest` — round-trip encrypt/decrypt of a ~400 MB generated file
None at the unit-test level after the `Azaion.Test` project was removed in cycle 2. `ToHash` is exercised end-to-end through every login / register e2e test (`e2e/Azaion.E2E/Tests/`).
@@ -63,5 +63,7 @@ PostgreSQL via `IDbFactory`.
- Read operations use the read-only DB connection; writes use the admin connection.
## Tests
- `Azaion.Test/UserServiceTest.cs` — unit/integration tests against the live test database (hardware-binding tests removed by AZ-197)
- `e2e/Azaion.E2E/Tests/DeviceTests.cs` — e2e for AZ-196 device-provisioning ACs
- `e2e/Azaion.E2E/Tests/UserManagementTests.cs` and `LoginTests.cs` — e2e coverage for the rest of the user lifecycle (login, register, role change, enable/disable, delete, queue offsets)
(Unit-test coverage in `Azaion.Test/UserServiceTest.cs` was removed earlier with the AZ-197 hardware-binding cleanup; the `Azaion.Test` project itself was removed from the solution in cycle 2 once its only remaining file — `SecurityTest.cs` — was deleted with the encrypted-download stack.)
@@ -1,45 +0,0 @@
# Module: Azaion.Test.SecurityTest
## Purpose
xUnit tests for the `Security` encryption/decryption functionality.
## Public Interface
| Test | Description |
|------|-------------|
| `EncryptDecryptTest` | Round-trip encrypt/decrypt of a ~1 KB string; asserts decrypted output matches original |
| `EncryptDecryptLargeFileTest` | Round-trip encrypt/decrypt of a ~400 MB generated file; compares SHA-256 hashes of original and decrypted files |
## Internal Logic
- **EncryptDecryptTest**: creates a key via `Security.GetApiEncryptionKey`, encrypts a test string to a `MemoryStream`, decrypts back, compares with `FluentAssertions`.
- **EncryptDecryptLargeFileTest**: generates a large JSON file (4M numbers chunked), encrypts, decrypts to a new file, compares file hashes via `SHA256.HashDataAsync`.
Private helpers:
- `CompareFiles` — SHA-256 hash comparison of two files
- `CreateLargeFile` — generates a large file by serializing number dictionaries in 100K chunks
- `StringToStream` — converts a UTF-8 string to a `MemoryStream`
## Dependencies
- `Security` (encrypt/decrypt)
- `StreamExtensions.ConvertToString`
- `FluentAssertions`
- `Newtonsoft.Json`
- xUnit
## Consumers
None — test module.
## Data Models
None.
## Configuration
None.
## External Integrations
Local filesystem (creates/deletes `large.txt` and `large_decrypted.txt` during large file test).
## Security
None.
## Tests
This IS the test module.
@@ -1,39 +0,0 @@
# Module: Azaion.Test.UserServiceTest
## Purpose
xUnit integration test for `UserService.CheckHardwareHash` against a live PostgreSQL database.
## Public Interface
| Test | Description |
|------|-------------|
| `CheckHardwareHashTest` | Looks up a known user by email, then calls `CheckHardwareHash` with a hardware fingerprint string |
## Internal Logic
- Creates a `DbFactory` with hardcoded connection strings pointing to a remote PostgreSQL instance.
- Creates a `UserService` with that factory and a fresh `MemoryCache`.
- Fetches user `spielberg@azaion.com`, then calls `CheckHardwareHash` with a specific hardware string.
- No assertion — the test only verifies no exception is thrown.
## Dependencies
- `UserService`, `DbFactory`, `MemoryCache`
- `ConnectionStrings`, `OptionsWrapper`
- xUnit
## Consumers
None — test module.
## Data Models
None.
## Configuration
Hardcoded connection strings to `188.245.120.247:4312` (remote database).
## External Integrations
Live PostgreSQL database (remote server).
## Security
Contains hardcoded database credentials in source code. This is a security concern — credentials should be in test configuration or environment variables.
## Tests
This IS the test module.
+10 -74
View File
@@ -1,6 +1,8 @@
# Azaion Admin API — System Flows
> **Cycle 1 (2026-05-13) note** — F4 (Hardware Check) was deleted by AZ-197; F3 no longer depends on hardware. Two new flows were added: F8 Detection Classes CRUD (AZ-513), F9 Device Auto-Provisioning (AZ-196). F10 OTA Update Check & Publish (AZ-183) was reverted later the same day after the security audit (finding F-1) — the OTA delivery model itself was deemed obsolete; see `_docs/05_security/security_report.md` for context. F3's narrative was updated to drop the hardware-check step.
>
> **Cycle 2 (2026-05-14) note** — F3 (Encrypted Resource Download) and F6 (Installer Download) were removed entirely as obsolete. The encrypted-download support stack (`Security.GetApiEncryptionKey`, `EncryptTo`, `DecryptTo`, `ResourcesService.GetEncryptedResource`, `ResourcesService.GetInstaller`, `GetResourceRequest`, `WrongResourceName` (50)) and the installer config (`SuiteInstallerFolder`, `SuiteStageInstallerFolder`) all went with them. See `_docs/02_document/architecture.md` ADR-003 (retired).
## Flow Inventory
@@ -8,10 +10,10 @@
|---|-----------|---------|-------------------|-------------|
| F1 | User Login | POST /login | Admin API, User Mgmt, Auth & Security | High |
| F2 | User Registration | POST /users | Admin API, User Mgmt | High |
| F3 | Encrypted Resource Download | POST /resources/get | Admin API, Auth, User Mgmt, Resource Mgmt | High |
| ~~F3~~ | ~~Encrypted Resource Download~~ | ~~POST /resources/get~~ | — | **REMOVED — cycle 2 (obsolete)** |
| ~~F4~~ | ~~Hardware Check~~ | ~~POST /resources/check~~ | — | **REMOVED — AZ-197** |
| F5 | Resource Upload | POST /resources | Admin API, Resource Mgmt | Medium |
| F6 | Installer Download | GET /resources/get-installer | Admin API, Auth, Resource Mgmt | Medium |
| ~~F6~~ | ~~Installer Download~~ | ~~GET /resources/get-installer~~ | — | **REMOVED — cycle 2 (obsolete)** |
| F7 | User Management (CRUD) | Various /users/* | Admin API, User Mgmt | Medium |
| F8 | Detection Classes CRUD *(AZ-513)* | POST/PATCH/DELETE /classes | Admin API, DetectionClassService | High |
| F9 | Device Auto-Provisioning *(AZ-196)* | POST /devices | Admin API, User Mgmt | High |
@@ -23,10 +25,8 @@
|------|-----------|-----------------|
| F1 | — | All other flows (produces JWT token) |
| F2 | — | F1, F9 (creates user records — including device users via F9) |
| F3 | F1 (requires JWT) | — (post-AZ-197: no hardware-binding dependency) |
| F5 | F1 (requires JWT) | F3 (uploaded resources are later downloaded) |
| F6 | F1 (requires JWT) | — |
| F7 | F1 (requires JWT, ApiAdmin role) | F3 (user data) |
| F5 | F1 (requires JWT) | — |
| F7 | F1 (requires JWT, ApiAdmin role) | — |
| F8 | F1 (requires JWT, ApiAdmin role) | UI Detection Classes table |
| F9 | F1 (requires JWT, ApiAdmin role) | F2 (writes a user row, but reuses `RegisterUser` end-to-end), F1 (provisioned devices later log in) |
@@ -112,48 +112,9 @@ sequenceDiagram
---
## Flow F3: Encrypted Resource Download
## Flow F3: Encrypted Resource Download — REMOVED (cycle 2, 2026-05-14)
> **Updated by AZ-197 (2026-05-13)** — the hardware-binding precondition and the `CheckHardwareHash` / `GetHWHash` steps were removed; the encryption key is now derived from `email + password` only. The diagram below reflects the post-cycle-1 path.
### Description
An authenticated user requests a resource file. The system derives a per-user encryption key from email + password, encrypts the file with AES-256-CBC, and streams the encrypted content.
### Preconditions
- User is authenticated (JWT)
- Resource file exists on server
### Sequence Diagram
```mermaid
sequenceDiagram
participant Client
participant API as Admin API
participant Auth as AuthService
participant Sec as Security
participant RS as ResourcesService
participant FS as Filesystem
Client->>API: POST /resources/get {password, fileName}
API->>Auth: GetCurrentUser()
Auth-->>API: User
API->>Sec: GetApiEncryptionKey(email, password)
Sec-->>API: AES key string
API->>RS: GetEncryptedResource(folder, fileName, key)
RS->>FS: Read file
FS-->>RS: FileStream
RS->>Sec: EncryptTo(stream, key)
Sec-->>RS: Encrypted MemoryStream
RS-->>API: Stream
API-->>Client: 200 OK (application/octet-stream)
```
### Error Scenarios
| Error | Where | Detection | Recovery |
|-------|-------|-----------|----------|
| Not authenticated | API | No/invalid JWT | 401 Unauthorized |
| File not found | ResourcesService | FileStream throws | 500 Internal Server Error |
The `POST /resources/get/{dataFolder?}` endpoint and its supporting stack (`Security.GetApiEncryptionKey`, `Security.EncryptTo`, `Security.DecryptTo`, `ResourcesService.GetEncryptedResource`, `GetResourceRequest` DTO + validator, `ExceptionEnum.WrongResourceName` (50)) were removed as obsolete. Per-user file encryption is no longer part of the system; resource files are now stored as plain bytes and only ever leave the server through the upload (F5) and admin clear paths. ADR-003 in `architecture.md` was retired in the same change.
---
@@ -195,34 +156,9 @@ sequenceDiagram
---
## Flow F6: Installer Download
## Flow F6: Installer Download — REMOVED (cycle 2, 2026-05-14)
### Description
An authenticated user downloads the latest Azaion Suite installer (production or staging).
### Preconditions
- User is authenticated (JWT)
- Installer file exists on server
### Sequence Diagram
```mermaid
sequenceDiagram
participant Client
participant API as Admin API
participant Auth as AuthService
participant RS as ResourcesService
participant FS as Filesystem
Client->>API: GET /resources/get-installer
API->>Auth: GetCurrentUser()
Auth-->>API: User (not null)
API->>RS: GetInstaller(isStage: false)
RS->>FS: Scan for AzaionSuite.Iterative*
FS-->>RS: FileInfo
RS-->>API: (name, FileStream)
API-->>Client: 200 OK (application/octet-stream)
```
The `GET /resources/get-installer` and `GET /resources/get-installer/stage` endpoints, the `ResourcesService.GetInstaller` method, the `ResourcesConfig.SuiteInstallerFolder` / `SuiteStageInstallerFolder` configuration properties, and their environment-variable rows in every config artifact (`appsettings.json`, `.env.example`, `secrets/*.public.env`, `docker-compose.test.yml`) were removed. The installer-shipping era is over in the target architecture (browser SaaS + fTPM Jetsons); installer artefacts are no longer served from the Admin API.
---
+32 -40
View File
@@ -184,51 +184,17 @@
---
### FT-P-09: Download Encrypted Resource
### FT-P-09: Download Encrypted Resource — OBSOLETE (cycle 2, 2026-05-14)
**Summary**: Authenticated user downloads an encrypted resource file.
**Traces to**: AC-14, AC-18
**Category**: Resource Distribution
The `POST /resources/get/{dataFolder?}` endpoint, the `Security.GetApiEncryptionKey` / `EncryptTo` helpers, the `ResourcesService.GetEncryptedResource` method, the `GetResourceRequest` DTO, and the e2e tests `Encrypted_download_returns_octet_stream_and_non_empty_body` (in `ResourceTests.cs`) and `Per_user_encryption_produces_distinct_ciphertext_for_same_file` (in `SecurityTests.cs`) were all removed. The endpoint now returns 404 — verified by FT-N-16 below.
**Preconditions**:
- User authenticated, hardware bound, resource file uploaded
**Input data**: `{"password":"validpwd1","hardware":"test-hw-001","fileName":"test.txt"}`
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | POST /resources/get with credentials | HTTP 200, Content-Type: application/octet-stream, non-empty body |
**Expected outcome**: HTTP 200 with encrypted binary content
**Max execution time**: 10s
ID retained for traceability stability; do not regenerate the spec body until a full `/test-spec` rerun.
---
### FT-P-10: Encryption Round-Trip Verification
### FT-P-10: Encryption Round-Trip Verification — OBSOLETE (cycle 2, 2026-05-14)
**Summary**: Downloaded encrypted resource decrypts to original file content.
**Traces to**: AC-15, AC-19
**Category**: Resource Distribution
**Preconditions**:
- Known file uploaded, user credentials known
**Input data**: Original file content, user email, password, hardware hash
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | Upload known file | HTTP 200 |
| 2 | Download encrypted file via API | HTTP 200, encrypted bytes |
| 3 | Derive AES key from email + password + hwHash | Key bytes |
| 4 | Decrypt downloaded content with derived key | Decrypted bytes |
| 5 | Compare decrypted bytes with original | Byte-level equality |
**Expected outcome**: Decrypted content matches original file exactly
**Max execution time**: 10s
Same removal as FT-P-09. Additionally `Security.DecryptTo` and the e2e test `Encryption_round_trip_decrypt_matches_original_bytes` (in `ResourceTests.cs`) are gone. ID retained for traceability stability.
---
@@ -487,12 +453,38 @@ The following legacy entries describe behaviour removed by AZ-197 (admin-side ha
- FT-P-04 (First Hardware Check Stores Fingerprint) — superseded; the `POST /resources/check` endpoint and the hardware-store side-effect were removed.
- FT-P-05 (Subsequent Hardware Check Matches) — superseded; same endpoint removed.
- FT-N-06 (Hardware Mismatch) — superseded; the `HardwareIdMismatch` / error code 40 path no longer exists in `ExceptionEnum`.
- FT-P-09 / FT-P-10 wire shape — the `hardware` field on `POST /resources/get/{dataFolder}` is no longer required; the encryption key is now derived from `email + password` only. The tests still pass without the field; do not regenerate spec bodies until a full `/test-spec` rerun.
- FT-P-09 / FT-P-10 — fully obsolete after the cycle-2 cleanup; the endpoint, support code, and corresponding e2e tests are gone (see the FT-P-09 / FT-P-10 stubs above and FT-N-16 below).
See `_docs/03_implementation/batch_06_report.md` for the full AZ-197 implementation rationale and the wire-compat policy decision (drop entirely).
---
### Cycle-2 Cleanup (2026-05-14) — Obsolete Resource Endpoints Removed
#### FT-N-16: Removed Resource Endpoints Return 404
**Summary**: After the cycle-2 cleanup, the three obsolete resource endpoints are no longer routed and return 404.
**Traces to**: Cycle-2 AC-1, Cycle-2 AC-2, Cycle-2 AC-3
**Category**: Negative — Removed Endpoints
**Preconditions**:
- Caller authenticated as any user (404 must precede any auth check, since the route is gone)
**Steps**:
| Step | Consumer Action | Expected System Response |
|------|----------------|------------------------|
| 1 | `POST /resources/get` (with or without body) | HTTP 404 |
| 2 | `POST /resources/get/somefolder` | HTTP 404 |
| 3 | `GET /resources/get-installer` | HTTP 404 |
| 4 | `GET /resources/get-installer/stage` | HTTP 404 |
**Expected outcome**: each request returns HTTP 404 (not 401, not 405); no `Security.GetApiEncryptionKey` / `EncryptTo` invocation observable in logs.
**Notes**: this is a parallel to FT-N-15 (which covers the AZ-197 endpoint removals). Together they enumerate every route that has been retired in cycles 1 and 2.
---
### Detection Classes CRUD (AZ-513)
#### FT-P-14: POST /classes Creates Detection Class
+10 -20
View File
@@ -10,13 +10,15 @@
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | GET /users (no JWT) | HTTP 401 |
| 2 | POST /resources/get (no JWT) | HTTP 401 |
| 3 | POST /resources/check (no JWT) | HTTP 401 |
| 4 | GET /resources/get-installer (no JWT) | HTTP 401 |
| 5 | PUT /users/role (no JWT) | HTTP 401 |
| 6 | DELETE /users (no JWT) | HTTP 401 |
| 2 | POST /resources/{folder} upload (no JWT) | HTTP 401 |
| 3 | GET /resources/list/{folder} (no JWT) | HTTP 401 |
| 4 | PUT /users/{email}/set-role/{role} (no JWT) | HTTP 401 |
| 5 | DELETE /users/{email} (no JWT) | HTTP 401 |
| 6 | POST /classes (no JWT) | HTTP 401 |
**Pass criteria**: All endpoints return HTTP 401 for unauthenticated requests
**Pass criteria**: All remaining protected endpoints return HTTP 401 for unauthenticated requests.
> Earlier revisions of this scenario also covered `POST /resources/get`, `POST /resources/check`, and `GET /resources/get-installer`. Those endpoints were removed (AZ-197 / cycle 2) and now return 404 — see FT-N-15 (AZ-197 routes) and FT-N-16 (cycle-2 routes) in `blackbox-tests.md`.
---
@@ -71,21 +73,9 @@
---
### NFT-SEC-05: Encryption Key Uniqueness
### NFT-SEC-05: Encryption Key Uniqueness — OBSOLETE (cycle 2, 2026-05-14)
**Summary**: Different users produce different encryption keys for the same resource.
**Traces to**: AC-19
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Upload test file | HTTP 200 |
| 2 | Download encrypted file as User A | Encrypted bytes A |
| 3 | Download same file as User B (different credentials + hardware) | Encrypted bytes B |
| 4 | Compare encrypted bytes A and B | Different |
**Pass criteria**: Encrypted outputs differ between users
The `POST /resources/get/{dataFolder?}` endpoint that this test exercised was removed along with `Security.GetApiEncryptionKey` / `EncryptTo` / `DecryptTo` and `ResourcesService.GetEncryptedResource`. Per-user resource encryption is no longer part of the system. ID retained for traceability stability; do not regenerate the spec body until a full `/test-spec` rerun.
---
+20 -1
View File
@@ -43,8 +43,9 @@
|----------|-----------|---------|-------------|-----------|
| Acceptance Criteria (baseline) | 19 | 19 | 0 | 100% |
| Acceptance Criteria (cycle 1) | 24 | 24 | 0 | 100% |
| Acceptance Criteria (cycle 2) | 6 | 6 | 0 | 100% |
| Restrictions | 8 | 5 | 3 | 63% |
| **Total** | **51** | **48** | **3** | **94%** |
| **Total** | **57** | **54** | **3** | **95%** |
## Uncovered Items Analysis
@@ -118,3 +119,21 @@ The matrix rows below are kept for ID stability but no longer reflect production
| AC-11 (Subsequent hardware check validates) | Obsoleted by AZ-197 — endpoint removed |
| AC-12 (Hardware mismatch returns code 40) | Obsoleted by AZ-197 — `ExceptionEnum` value removed |
| AC-19 (Encryption key derived from email+password+hw) | Partially obsoleted — derivation is now `email + password` only |
## Cycle 2 Cleanup (2026-05-14) — Obsolete Resource Endpoints Removed
The encrypted-download and installer-download endpoints were removed as obsolete. Affected matrix rows below are kept for ID stability but the underlying behaviour is gone; they are superseded by FT-N-16 in `blackbox-tests.md`.
| Removed surface | Endpoint(s) | Affected legacy entries | Status |
|-----------------|-------------|-------------------------|--------|
| Per-user encrypted resource download | `POST /resources/get/{dataFolder?}` | AC-14 (AES-256-CBC encryption), AC-15 (round-trip), AC-19 (key derivation), FT-P-09, FT-P-10 | **Reverted** — endpoint deleted; `Security.GetApiEncryptionKey` / `EncryptTo` / `DecryptTo` and `ResourcesService.GetEncryptedResource` deleted; `GetResourceRequest` DTO deleted; e2e tests `Encrypted_download_returns_octet_stream_and_non_empty_body` and `Encryption_round_trip_decrypt_matches_original_bytes` deleted from `ResourceTests.cs`; e2e test `Per_user_encryption_produces_distinct_ciphertext_for_same_file` deleted from `SecurityTests.cs`; `Azaion.Test/SecurityTest.cs` deleted (and the now-empty `Azaion.Test` project removed from the solution). |
| Installer download (production + staging) | `GET /resources/get-installer`, `GET /resources/get-installer/stage` | AC-23 (latest installer), `ResourcesConfig.SuiteInstallerFolder` / `SuiteStageInstallerFolder` references | **Reverted** — endpoints deleted; `ResourcesService.GetInstaller` deleted; both config properties removed from `appsettings.json`, `.env.example`, `secrets/staging.public.env`, `secrets/production.public.env`, and `docker-compose.test.yml`. No e2e tests had been written for these endpoints, so no tests required removal. |
| AC ID | Acceptance Criterion | Test IDs | Coverage |
|-------|---------------------|----------|----------|
| Cycle-2 AC-1 | `POST /resources/get/{dataFolder?}` returns 404 | FT-N-16 | Covered |
| Cycle-2 AC-2 | `GET /resources/get-installer` returns 404 | FT-N-16 | Covered |
| Cycle-2 AC-3 | `GET /resources/get-installer/stage` returns 404 | FT-N-16 | Covered |
| Cycle-2 AC-4 | `ExceptionEnum` no longer carries `WrongResourceName` (50); the gap is preserved | — | Build/CI invariant — verified by enum read |
| Cycle-2 AC-5 | `Azaion.Test` project no longer in solution; build is clean | — | Build invariant — `dotnet build Azaion.AdminApi.sln` clean post-cleanup |
| Cycle-2 AC-6 | E2E suite passes after the test deletions above | All e2e tests | Covered by Step 11 Run Tests post-cleanup (2026-05-14) |