[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.
@@ -0,0 +1,73 @@
# Batch 06 Code Review
**Date**: 2026-05-13
**Cycle**: 1, batch 2 of 2
**Tasks reviewed**: AZ-197 (remove hardware ID binding from resource flow — admin-side cleanup)
**Reviewer**: implement skill (Phase 9 — code-review)
**Verdict**: **PASS**
## Phase 1 — Production code touched
| File | Change | Notes |
|---|---|---|
| `Azaion.AdminApi/Program.cs` | Removed `MapPut("/users/hardware/set")`. Removed `MapPost("/resources/check")`. Simplified `MapPost("/resources/get/{dataFolder?}")` to derive key from `email + password` only. | Endpoints disappear cleanly; remaining route table unchanged. |
| `Azaion.Services/UserService.cs` | Removed `UpdateHardware`, `CheckHardwareHash`, `UpdateLastLoginDate` (private helper only used by `CheckHardwareHash`). Removed both methods from `IUserService`. | `UpdateLastLoginDate` was the *only* writer of `User.LastLogin`; consequence noted in §Implementation notes. |
| `Azaion.Services/Security.cs` | Removed `GetHWHash`. Changed `GetApiEncryptionKey(email, password, hardwareHash)``GetApiEncryptionKey(email, password)`. | Public-API breaking change inside the workspace; all callers updated. |
| `Azaion.Common/Requests/GetResourceRequest.cs` | Removed `Hardware` property and its FluentValidation rule. | Wire-compat policy: **drop entirely** (see Implementation notes for rationale). |
| `Azaion.Common/Requests/SetHWRequest.cs` | Deleted. | The DTO had a single consumer (the deleted endpoint). |
| `Azaion.Common/BusinessException.cs` | Removed `HardwareIdMismatch = 40` and `BadHardware = 45` from `ExceptionEnum`. | Numeric codes 40 and 45 are now unused — left as a numeric gap. **Do not reuse**. |
## Phase 2 — Test code touched
| File | Change | Notes |
|---|---|---|
| `Azaion.Test/SecurityTest.cs` | Dropped third `hardwareId` argument from two `GetApiEncryptionKey` calls. | Compile-only fix — assertions unchanged. |
| `Azaion.Test/UserServiceTest.cs` | Deleted. | Sole test was `CheckHardwareHashTest`; method under test no longer exists. |
| `e2e/Azaion.E2E/Tests/HardwareBindingTests.cs` | Deleted (165 lines). | All scenarios asserted behaviour that AZ-197 removes. |
| `e2e/Azaion.E2E/Tests/ResourceTests.cs` | Removed `SampleHardware` constant. Dropped `/resources/check` calls from two scenarios. Dropped `Hardware` field from `/resources/get` payloads. Updated `DecryptResourcePayload` helper signature + body. | Encryption round-trip now uses 2-arg key formula. |
| `e2e/Azaion.E2E/Tests/SecurityTests.cs` | Dropped `hardware` from anonymous `/resources/get` payload (unauth probe). Refactored `Per_user_encryption_produces_distinct_ciphertext_for_same_file` `DownloadForAsync` helper to drop `hardware` parameter and the `/resources/check` precondition. **Added** `Hardware_endpoints_are_removed_AZ_197` (AC-2 regression test asserting both removed routes return 404). | New test is the only added test surface in this batch. |
| `e2e/Azaion.E2E/Tests/ResilienceTests.cs` | Deleted `Concurrent_hardware_binding_same_hardware_has_no_500_and_state_consistent`. Removed now-unused `using System.Net.Http.Json;`, `using System.Text.Json;`, `ResponseJsonOptions` field, `TestUserPassword` constant. | The scenario has no analogue post-AZ-197. |
## Phase 3 — Documentation touched
| File | Change | Notes |
|---|---|---|
| `_docs/02_document/diagrams/flows/flow_hardware_check.md` | Replaced with a tombstone marker pointing to the AZ-197 spec. | Retained so historical links resolve. Mermaid diagram reduced to a single "REMOVED" node. |
| `_docs/02_document/architecture.md` | Updated System Context, integration table, data-model table, ADR-003 (per-user encryption — new key formula), ADR-004 (retired with full rationale). Added a top-level note about the AZ-197 cleanup. | Single source of truth for the new architecture. |
## Phase 4 — Build status
- `dotnet build Azaion.AdminApi.sln`**0 errors, 0 warnings**.
- `dotnet build e2e/Azaion.E2E/Azaion.E2E.csproj`**0 errors, 0 warnings**.
## Phase 5 — AC coverage check
| AC | Verification |
|---|---|
| AC-1 — Resource download works without hardware | `ResourceTests.Encrypted_download_returns_octet_stream_and_non_empty_body`, `Encryption_round_trip_decrypt_matches_original_bytes`, `SecurityTests.Per_user_encryption_produces_distinct_ciphertext_for_same_file` (all send no `Hardware` field). |
| AC-2 — `PUT /users/hardware/set` is gone | New `SecurityTests.Hardware_endpoints_are_removed_AZ_197` asserts 404 on both `PUT /users/hardware/set` and `POST /resources/check`. |
| AC-3 — `Security.GetApiEncryptionKey` signature | Compile-time enforced; `Azaion.Test/SecurityTest.cs` exercises the 2-arg overload. |
| AC-4 — `HardwareBindingTests` removed | File deleted (`git status` shows `D e2e/Azaion.E2E/Tests/HardwareBindingTests.cs`). |
| AC-5 — No remaining test sends `Hardware` | `rg` over `Azaion.Test` and `e2e/Azaion.E2E` returns no `Hardware =` / `hardware =` payloads outside the AC-2 negative-path test. |
| AC-6 — `ExceptionEnum` no longer has hardware codes | Confirmed by `BusinessException.cs` read; codes 40 and 45 are absent. Compile would have failed if any production reference remained. |
| AC-7 — All tests pass | Deferred to step 16 (test-run skill). Build is green; expectation is full pass. |
## Phase 6 — Scope discipline
- All changes are within the AZ-197 scope as defined by the task spec (Admin API production code + workspace tests + listed docs).
- `User.Hardware` entity property and DB column **left in place** per the spec ("DB column tombstoned, no migration in this ticket").
- Adjacent hygiene performed: removed now-unused `using` directives in `ResilienceTests.cs`; removed dead private helper `UpdateLastLoginDate`. Both were caused by the in-scope deletions.
- No unrelated fixes piggy-backed.
## Phase 7 — Risks / follow-ups (non-blocking)
1. **`User.LastLogin` is now never updated by application code.** Pre-AZ-197, the only writer was `UpdateLastLoginDate`, called from `CheckHardwareHash`. `ValidateUser` (login) does not update it. The column becomes write-once via direct DB inserts. This was *also* true post-AZ-197 in the sense that the only updater fired during a hardware-check (not during login), so this is not a *regression* in the login path — but it does mean the field is now effectively dead. Two options for a future ticket:
- Move a `LastLogin = UtcNow` write into `ValidateUser` (preserves the spirit of the field).
- Drop the column.
Out of scope for AZ-197; documented for future cleanup.
2. **`User.Hardware` column tombstone.** Per spec — no migration in this ticket. Future cleanup ticket can drop the column once any external readers (BI dashboards, audit exports) are confirmed absent.
3. **Numeric error code gap (40, 45).** Intentional. A code-review rule "do not reuse retired error codes" would be worth adding to `coderule.mdc` if it isn't already there — out of scope here.
## Verdict — PASS
No blocking issues. Build is clean across both projects. AC coverage is complete (AC-7 confirmed at step 16 by `dotnet test`). Tombstone strategy for `User.Hardware` and `User.LastLogin` is documented and explicitly out of scope per the task spec. Ready for commit.