# Batch 06 Implementation Report **Date**: 2026-05-13 **Cycle**: 1, batch 2 of 2 **Tasks**: AZ-197 (remove hardware ID binding from resource flow — admin-side cleanup) **Total complexity**: 3 points **Implementer**: implement skill (autodev cycle 1) ## Why this batch is one-task Batch 5 was additive (AZ-513, AZ-196, AZ-183 — three new endpoints + a new table). Batch 6 is the destructive complement: it removes a cross-cutting feature (hardware fingerprint binding) that touches services, DTOs, business-exception codes, route table, and tests across two projects. Splitting destructive cleanup from new-feature work was a deliberate batching decision (recorded in `batch_05_report.md` §"AZ-197 not in this batch"). It makes the diff focused and reviewable, and it keeps the risk envelope of the additive batch independent. ## Wire-compat policy decision (recorded per task-spec ¶6 of "Included") The AZ-197 spec offers two options for the `Hardware` field on resource requests: **drop entirely** vs. **accept-and-ignore**. **Decision: drop entirely.** Rationale: - The Loader workspace is architecturally retired (`suite/_docs/_repo-config.yaml` `unresolved:loader-retirement-arch-doc`). There is no in-flight legacy Loader client that needs a forgiving wire format. - New edge devices ship as fTPM-secured Jetsons running freshly-deployed software; new SaaS clients send what we tell them to send. - "Accept-and-ignore" would require keeping the field on `GetResourceRequest`, the validator, and the Loader-shaped DTO docs — purely as a tombstone. The maintenance cost is non-zero and the benefit is zero. - A single 400 on a stale client (if any exists) is preferable to silently accepting a hardware fingerprint that the server now ignores. A loud, fast failure is the right behaviour. This decision is also reflected in the e2e `SecurityTests.Hardware_endpoints_are_removed_AZ_197` regression test, which asserts the routes return 404 (not 200-with-warning). ## Implementation summary by file ### Production code - **`Azaion.AdminApi/Program.cs`** — removed `MapPut("/users/hardware/set", ...)` (4 lines including `.RequireAuthorization`/`.WithSummary`). Removed `MapPost("/resources/check", ...)` (8 lines). Simplified `MapPost("/resources/get/{dataFolder?}", ...)`: dropped the `IUserService` parameter and the `await userService.CheckHardwareHash(...)` call; `Security.GetApiEncryptionKey` now invoked with `(user.Email, request.Password)`. - **`Azaion.Services/UserService.cs`** — removed `UpdateHardware` and `CheckHardwareHash` methods + their `IUserService` declarations. Removed the private `UpdateLastLoginDate(User user)` helper (sole caller was `CheckHardwareHash`). The interface now declares 9 methods (was 11). - **`Azaion.Services/Security.cs`** — `GetApiEncryptionKey` signature changed from `(string email, string password, string? hardwareHash)` to `(string email, string password)`. Implementation removes the `-{hardwareHash}` segment from the hashed input. `GetHWHash` deleted entirely. - **`Azaion.Common/Requests/GetResourceRequest.cs`** — removed the `Hardware` string property. The companion `GetResourceRequestValidator` (same file) lost its `RuleFor(x => x.Hardware)` rule. - **`Azaion.Common/Requests/SetHWRequest.cs`** — file deleted. - **`Azaion.Common/BusinessException.cs`** — removed two `ExceptionEnum` entries: `HardwareIdMismatch = 40` and `BadHardware = 45`. The numeric codes are intentionally left as a gap (40 and 45 are now reserved-by-history, not to be reused). ### Test code - **`Azaion.Test/SecurityTest.cs`** — dropped the third positional `hardwareId` argument from two `GetApiEncryptionKey` invocations. - **`Azaion.Test/UserServiceTest.cs`** — file deleted (sole test asserted the removed `CheckHardwareHash`). - **`e2e/Azaion.E2E/Tests/HardwareBindingTests.cs`** — file deleted (165 lines). Every scenario asserted behaviour AZ-197 removes. - **`e2e/Azaion.E2E/Tests/ResourceTests.cs`** — purged hardware references: removed `SampleHardware` constant, removed `/resources/check` precondition calls, dropped `Hardware = ...` from `/resources/get` payloads, simplified `DecryptResourcePayload` helper (no `hardware` parameter; key derivation matches the new `Security.GetApiEncryptionKey`). - **`e2e/Azaion.E2E/Tests/SecurityTests.cs`** — dropped `hardware` from anonymous payload in `Unauthenticated_requests_to_protected_endpoints_return_401`. Refactored `DownloadForAsync` helper inside `Per_user_encryption_produces_distinct_ciphertext_for_same_file` to drop the `hardware` parameter and the `/resources/check` step. **Added** `Hardware_endpoints_are_removed_AZ_197` — a regression test asserting both `PUT /users/hardware/set` and `POST /resources/check` return 404. - **`e2e/Azaion.E2E/Tests/ResilienceTests.cs`** — deleted `Concurrent_hardware_binding_same_hardware_has_no_500_and_state_consistent`. Adjacent hygiene: dropped now-unused `System.Net.Http.Json` and `System.Text.Json` `using` directives, plus the orphaned `ResponseJsonOptions` field and `TestUserPassword` constant. ### Documentation - **`_docs/02_document/diagrams/flows/flow_hardware_check.md`** — converted to a tombstone (header note + minimal "REMOVED" Mermaid). Retained so historical cross-references resolve. - **`_docs/02_document/architecture.md`** — five edits: 1. Top-level System Context note explaining the AZ-197 cleanup. 2. Integration table: dropped "hardware check" from the Azaion Suite client row. 3. Data-model entity table: marked `User.Hardware` as a tombstoned column. 4. ADR-003 (Per-User Resource Encryption) updated with the new key formula and an explicit cross-reference to ADR-004. 5. ADR-004 (Hardware Fingerprint Binding) renamed to "RETIRED (AZ-197)" with full retirement rationale (sealed Jetson + SaaS architecture, fTPM key storage). ## Notes / known consequences (non-blocking) 1. **`User.LastLogin` is now never written by the application.** Pre-AZ-197, its only writer was `UpdateLastLoginDate`, called inside `CheckHardwareHash`. The login path (`ValidateUser`) never wrote it. The field is therefore now effectively dead. **Out of scope for AZ-197**; flagged in batch 6 review for a future ticket. 2. **`User.Hardware` column is a nullable tombstone.** Spec explicitly forbids the migration in this ticket (workspace rule "no rename without confirmation" + "avoid renaming if possible"). 3. **Numeric error-code gap.** Codes 40 (`HardwareIdMismatch`) and 45 (`BadHardware`) are now unused. Intentional — never reuse retired error codes. Worth a `coderule.mdc` line if not already covered (out of scope here). ## Build status | Target | Result | |---|---| | `dotnet build Azaion.AdminApi.sln` | 0 errors, 0 warnings | | `dotnet build e2e/Azaion.E2E/Azaion.E2E.csproj` | 0 errors, 0 warnings | ## Test status Full `dotnet test` runs in **step 16** under the test-run skill. Build is green; expectation is a full pass with one fewer test class (`HardwareBindingTests`) and one fewer test in `ResilienceTests`, plus the new `SecurityTests.Hardware_endpoints_are_removed_AZ_197` test. ## Tracker linkage - AZ-197 will transition `In Progress → In Testing` after the batch-6 commit lands; final transition to `Done` after the test-run step confirms the full suite passes.