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>
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.yamlunresolved: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— removedMapPut("/users/hardware/set", ...)(4 lines including.RequireAuthorization/.WithSummary). RemovedMapPost("/resources/check", ...)(8 lines). SimplifiedMapPost("/resources/get/{dataFolder?}", ...): dropped theIUserServiceparameter and theawait userService.CheckHardwareHash(...)call;Security.GetApiEncryptionKeynow invoked with(user.Email, request.Password). -
Azaion.Services/UserService.cs— removedUpdateHardwareandCheckHardwareHashmethods + theirIUserServicedeclarations. Removed the privateUpdateLastLoginDate(User user)helper (sole caller wasCheckHardwareHash). The interface now declares 9 methods (was 11). -
Azaion.Services/Security.cs—GetApiEncryptionKeysignature changed from(string email, string password, string? hardwareHash)to(string email, string password). Implementation removes the-{hardwareHash}segment from the hashed input.GetHWHashdeleted entirely. -
Azaion.Common/Requests/GetResourceRequest.cs— removed theHardwarestring property. The companionGetResourceRequestValidator(same file) lost itsRuleFor(x => x.Hardware)rule. -
Azaion.Common/Requests/SetHWRequest.cs— file deleted. -
Azaion.Common/BusinessException.cs— removed twoExceptionEnumentries:HardwareIdMismatch = 40andBadHardware = 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 positionalhardwareIdargument from twoGetApiEncryptionKeyinvocations. -
Azaion.Test/UserServiceTest.cs— file deleted (sole test asserted the removedCheckHardwareHash). -
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: removedSampleHardwareconstant, removed/resources/checkprecondition calls, droppedHardware = ...from/resources/getpayloads, simplifiedDecryptResourcePayloadhelper (nohardwareparameter; key derivation matches the newSecurity.GetApiEncryptionKey). -
e2e/Azaion.E2E/Tests/SecurityTests.cs— droppedhardwarefrom anonymous payload inUnauthenticated_requests_to_protected_endpoints_return_401. RefactoredDownloadForAsynchelper insidePer_user_encryption_produces_distinct_ciphertext_for_same_fileto drop thehardwareparameter and the/resources/checkstep. AddedHardware_endpoints_are_removed_AZ_197— a regression test asserting bothPUT /users/hardware/setandPOST /resources/checkreturn 404. -
e2e/Azaion.E2E/Tests/ResilienceTests.cs— deletedConcurrent_hardware_binding_same_hardware_has_no_500_and_state_consistent. Adjacent hygiene: dropped now-unusedSystem.Net.Http.JsonandSystem.Text.Jsonusingdirectives, plus the orphanedResponseJsonOptionsfield andTestUserPasswordconstant.
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:- Top-level System Context note explaining the AZ-197 cleanup.
- Integration table: dropped "hardware check" from the Azaion Suite client row.
- Data-model entity table: marked
User.Hardwareas a tombstoned column. - ADR-003 (Per-User Resource Encryption) updated with the new key formula and an explicit cross-reference to ADR-004.
- 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)
-
User.LastLoginis now never written by the application. Pre-AZ-197, its only writer wasUpdateLastLoginDate, called insideCheckHardwareHash. 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. -
User.Hardwarecolumn is a nullable tombstone. Spec explicitly forbids the migration in this ticket (workspace rule "no rename without confirmation" + "avoid renaming if possible"). -
Numeric error-code gap. Codes 40 (
HardwareIdMismatch) and 45 (BadHardware) are now unused. Intentional — never reuse retired error codes. Worth acoderule.mdcline 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 Testingafter the batch-6 commit lands; final transition toDoneafter the test-run step confirms the full suite passes.