[AZ-197] Remove hardware ID binding from resource flow

Sealed-Jetson + SaaS architecture eliminates the credential-reuse-across-
machines threat that motivated hardware fingerprint binding. The binding's
only remaining effect was a real production failure mode on legitimate
hardware events.

Production:
- Drop PUT /users/hardware/set and POST /resources/check.
- Simplify POST /resources/get/{dataFolder?} (no Hardware field).
- Remove CheckHardwareHash, UpdateHardware, Security.GetHWHash.
- GetApiEncryptionKey signature: (email, password) — no hardwareHash.
- Drop SetHWRequest DTO and Hardware property from GetResourceRequest.
- Remove HardwareIdMismatch (40) and BadHardware (45) ExceptionEnum
  entries; numeric codes left as a gap, not for reuse.

Wire-compat policy: drop entirely (no Loader; no in-flight legacy
clients). Stale callers will see 404s, which is the right loud failure.

Tombstones:
- User.Hardware DB column kept (nullable, unused) — separate cleanup
  ticket for the migration per workspace "no rename without confirmation".
- User.LastLogin is now never written by app code (only writer was inside
  the deleted CheckHardwareHash); flagged in batch_06_review for a future
  ticket.

Tests:
- Delete e2e HardwareBindingTests (165 lines) and Azaion.Test
  UserServiceTest (sole test was CheckHardwareHashTest).
- Drop Hardware payloads + /resources/check preconditions from e2e
  ResourceTests, SecurityTests, ResilienceTests; drop hardwareId arg
  from Azaion.Test SecurityTest.
- Add SecurityTests.Hardware_endpoints_are_removed_AZ_197 (AC-2 regression
  asserting both removed routes return 404).

Docs:
- architecture.md: System Context note, ADR-003 new key formula, ADR-004
  retired with rationale.
- diagrams/flows/flow_hardware_check.md: tombstoned.

Also archives the four batch-1+batch-2 task files into _docs/02_tasks/done/
(file moves were missed by the batch_05 commit).

Code review: PASS — see _docs/03_implementation/reviews/batch_06_review.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-13 04:46:39 +03:00
parent 5ca9ccab2c
commit 5e90512987
22 changed files with 359 additions and 490 deletions
@@ -0,0 +1,92 @@
# 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.