Files
Oleksandr Bezdieniezhnykh 5e90512987 [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>
2026-05-13 04:46:39 +03:00

7.2 KiB

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.csGetApiEncryptionKey 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.