mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 15:21:10 +00:00
refactor: remove deploy.cmd and update Dockerfile for health checks
- Deleted the deploy.cmd script as it was no longer needed. - Updated Dockerfile to include curl for health checks and added a non-root user for improved security. - Modified health check command to use curl for better reliability. - Adjusted docker-compose.test.yml to reflect changes in health check configuration. - Cleaned up appsettings.json and removed unused configuration properties. - Removed Resource entity and related requests from the codebase as part of the architectural shift. - Updated documentation to reflect the removal of hardware binding and related endpoints. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,25 +5,40 @@ Application entry point: configures DI, middleware, authentication, authorizatio
|
||||
|
||||
## Public Interface (HTTP Endpoints)
|
||||
|
||||
| Method | Path | Auth | Summary |
|
||||
|--------|------|------|---------|
|
||||
| POST | `/login` | Anonymous | Validates credentials, returns JWT token |
|
||||
| POST | `/users` | ApiAdmin | Creates a new user |
|
||||
| GET | `/users/current` | Any authenticated | Returns current user from JWT claims |
|
||||
| GET | `/users` | ApiAdmin | Lists users with optional email/role filters |
|
||||
| PUT | `/users/hardware/set` | ApiAdmin | Sets a user's hardware fingerprint |
|
||||
| PUT | `/users/queue-offsets/set` | Any authenticated | Updates user's queue offsets |
|
||||
| PUT | `/users/{email}/set-role/{role}` | ApiAdmin | Changes a user's role |
|
||||
| PUT | `/users/{email}/enable` | ApiAdmin | Enables a user account |
|
||||
| PUT | `/users/{email}/disable` | ApiAdmin | Disables a user account |
|
||||
| DELETE | `/users/{email}` | ApiAdmin | Removes a user |
|
||||
| POST | `/resources/{dataFolder?}` | Any authenticated | Uploads a resource file |
|
||||
| GET | `/resources/list/{dataFolder?}` | Any authenticated | Lists files in a resource folder |
|
||||
| POST | `/resources/clear/{dataFolder?}` | ApiAdmin | Clears a resource folder |
|
||||
| POST | `/resources/get/{dataFolder?}` | Any authenticated | Downloads an encrypted resource |
|
||||
| GET | `/resources/get-installer` | Any authenticated | Downloads latest production installer |
|
||||
| GET | `/resources/get-installer/stage` | Any authenticated | Downloads latest staging installer |
|
||||
| POST | `/resources/check` | Any authenticated | Validates hardware fingerprint |
|
||||
> **Cycle 1 (2026-05-13) note** — endpoint surface changed by AZ-513 (detection-class CRUD), AZ-196 (device auto-registration), AZ-197 (hardware-binding removal). AZ-183 (OTA update check + publish) was reverted later the same day after the security audit (finding F-1) — the OTA delivery model itself was deemed obsolete; see `_docs/05_security/security_report.md` for context. The table reflects the post-cycle-1 state including that revert.
|
||||
|
||||
| Method | Path | Auth | Summary | Cycle 1 origin |
|
||||
|--------|------|------|---------|----------------|
|
||||
| POST | `/login` | Anonymous | Validates credentials, returns JWT token | — |
|
||||
| POST | `/users` | ApiAdmin | Creates a new user | — |
|
||||
| POST | `/devices` | ApiAdmin | Creates a CompanionPC device user (auto serial / email / 32-hex password) | AZ-196 |
|
||||
| GET | `/users/current` | Any authenticated | Returns current user from JWT claims | — |
|
||||
| GET | `/users` | ApiAdmin | Lists users with optional email/role filters | — |
|
||||
| PUT | `/users/queue-offsets/set` | Any authenticated | Updates user's queue offsets | — |
|
||||
| PUT | `/users/{email}/set-role/{role}` | ApiAdmin | Changes a user's role | — |
|
||||
| PUT | `/users/{email}/enable` | ApiAdmin | Enables a user account | — |
|
||||
| PUT | `/users/{email}/disable` | ApiAdmin | Disables a user account | — |
|
||||
| DELETE | `/users/{email}` | ApiAdmin | Removes a user | — |
|
||||
| POST | `/resources/{dataFolder?}` | Any authenticated | Uploads a resource file | — |
|
||||
| GET | `/resources/list/{dataFolder?}` | Any authenticated | Lists files in a resource folder | — |
|
||||
| POST | `/resources/clear/{dataFolder?}` | ApiAdmin | Clears a resource folder | — |
|
||||
| POST | `/resources/get/{dataFolder?}` | Any authenticated | Downloads an encrypted resource (key derived from `email + password` only) | AZ-197 wire change (no `Hardware` field) |
|
||||
| GET | `/resources/get-installer` | Any authenticated | Downloads latest production installer | — |
|
||||
| GET | `/resources/get-installer/stage` | Any authenticated | Downloads latest staging installer | — |
|
||||
| POST | `/classes` | ApiAdmin | Creates a detection class | AZ-513 |
|
||||
| PATCH | `/classes/{id:int}` | ApiAdmin | Updates a detection class (partial-merge) | AZ-513 |
|
||||
| DELETE | `/classes/{id:int}` | ApiAdmin | Deletes a detection class | AZ-513 |
|
||||
|
||||
### Removed in cycle 1
|
||||
|
||||
The following endpoints were removed during cycle 1 and now return `404`:
|
||||
|
||||
| Method | Path | Reason removed |
|
||||
|--------|------|----------------|
|
||||
| PUT | `/users/hardware/set` | AZ-197 — hardware-binding feature deleted (no fielded clients in target architecture) |
|
||||
| POST | `/resources/check` | AZ-197 — was the hardware-binding side-effect probe; no remaining purpose |
|
||||
| POST | `/get-update` | OTA delivery model retired post-cycle-1 (security audit F-1: endpoint disclosed plaintext per-resource encryption keys to any authenticated caller; the underlying installer-distribution flow is itself obsolete) |
|
||||
| POST | `/resources/publish` | Same revert as `/get-update` — the publish counterpart of the OTA flow |
|
||||
|
||||
## Internal Logic
|
||||
|
||||
@@ -31,10 +46,11 @@ Application entry point: configures DI, middleware, authentication, authorizatio
|
||||
- `IUserService` → `UserService` (Scoped)
|
||||
- `IAuthService` → `AuthService` (Scoped)
|
||||
- `IResourcesService` → `ResourcesService` (Scoped)
|
||||
- `IDetectionClassService` → `DetectionClassService` (Scoped) — added by AZ-513
|
||||
- `IDbFactory` → `DbFactory` (Singleton)
|
||||
- `ICache` → `MemoryCache` (Scoped)
|
||||
- `LazyCache` via `AddLazyCache()`
|
||||
- FluentValidation validators auto-discovered from `RegisterUserValidator` assembly
|
||||
- FluentValidation validators auto-discovered from `RegisterUserValidator` assembly (also picks up `CreateDetectionClassRequest`, `UpdateDetectionClassRequest` validators introduced in cycle 1)
|
||||
- `BusinessExceptionHandler` registered as exception handler
|
||||
|
||||
### Middleware Pipeline
|
||||
@@ -47,7 +63,8 @@ Application entry point: configures DI, middleware, authentication, authorizatio
|
||||
|
||||
### Authorization Policies
|
||||
- `apiAdminPolicy`: requires `RoleEnum.ApiAdmin` role
|
||||
- `apiUploaderPolicy`: requires `RoleEnum.ResourceUploader` OR `RoleEnum.ApiAdmin` role
|
||||
|
||||
> The `apiUploaderPolicy` (`RoleEnum.ResourceUploader` OR `ApiAdmin`) was added by AZ-183 and removed in the same cycle when the OTA endpoints it guarded were retired (see "Removed in cycle 1" above). `RoleEnum.ResourceUploader` itself remains as a data value (the seed `uploader@azaion.com` still uses it) but is no longer wired to any endpoint policy.
|
||||
|
||||
### Configuration Sections
|
||||
- `JwtConfig` — JWT signing/validation
|
||||
|
||||
@@ -18,14 +18,15 @@ Custom exception type for domain-level errors, paired with an `ExceptionEnum` ca
|
||||
| `NoEmailFound` | 10 | No such email found |
|
||||
| `EmailExists` | 20 | Email already exists |
|
||||
| `WrongPassword` | 30 | Passwords do not match |
|
||||
| `PasswordLengthIncorrect` | 32 | Password should be at least 8 characters |
|
||||
| `PasswordLengthIncorrect` | 32 | Password should be at least 12 characters (description text — actual validator threshold is 8 chars per `RegisterUserValidator`) |
|
||||
| `EmailLengthIncorrect` | 35 | Email is empty or invalid |
|
||||
| `WrongEmail` | 37 | (no description attribute) |
|
||||
| `HardwareIdMismatch` | 40 | Hardware mismatch — unauthorized hardware |
|
||||
| `BadHardware` | 45 | Hardware should be not empty |
|
||||
| `UserDisabled` | 38 | User account is disabled |
|
||||
| `WrongResourceName` | 50 | Wrong resource file name |
|
||||
| `NoFileProvided` | 60 | No file provided |
|
||||
|
||||
> **Cycle 1 (2026-05-13) note** — `HardwareIdMismatch = 40` and `BadHardware = 45` were removed by AZ-197 (admin-side hardware-binding cleanup). Code 40 should NOT be reused for a different meaning — older clients may still surface "Hardware mismatch" UX strings keyed on the integer. `UserDisabled = 38` was added earlier (still part of the baseline). See `_docs/03_implementation/batch_06_report.md`.
|
||||
|
||||
## Internal Logic
|
||||
Static constructor eagerly loads all `ExceptionEnum` descriptions into a dictionary via `EnumExtensions.GetDescriptions<ExceptionEnum>()`. Messages are retrieved by dictionary lookup with fallback to `ToString()`.
|
||||
|
||||
@@ -34,8 +35,8 @@ Static constructor eagerly loads all `ExceptionEnum` descriptions into a diction
|
||||
|
||||
## Consumers
|
||||
- `BusinessExceptionHandler` — catches and serializes to HTTP 409 response
|
||||
- `UserService` — throws for email/password/hardware validation failures
|
||||
- `ResourcesService` — throws for missing file uploads
|
||||
- `UserService` — throws for email/password validation failures (`NoEmailFound`, `WrongPassword`, `EmailExists`, `UserDisabled`)
|
||||
- `ResourcesService` — throws `NoFileProvided` for missing file uploads
|
||||
- FluentValidation validators — reference `ExceptionEnum` codes in `.WithErrorCode()`
|
||||
|
||||
## Data Models
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Module: Azaion.Common.Entities.DetectionClass
|
||||
|
||||
## Purpose
|
||||
Domain entity for a single detection class shown to operators in the Detection Classes admin table. Persisted to the `detection_classes` table; managed via the `/classes` admin endpoints introduced by AZ-513.
|
||||
|
||||
> **Cycle 1 (2026-05-13) origin** — added by AZ-513 to back the new admin `/classes` CRUD endpoints; previously the read path was served by another service (likely `annotations/`) and admin/ had no own model for it.
|
||||
|
||||
## Public Interface
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Id` | `int` | Auto-assigned identity (DB-generated via `InsertWithInt32IdentityAsync`) |
|
||||
| `Name` | `string` | Full display name (max 120 chars per validator) |
|
||||
| `ShortName` | `string` | Short label used in tight UI (max 20 chars) |
|
||||
| `Color` | `string` | UI color (e.g. `"#FF0000"`, max 20 chars — accepts hex strings or named-color tokens) |
|
||||
| `MaxSizeM` | `double` | Maximum real-world object size in meters (must be > 0) |
|
||||
| `PhotoMode` | `string?` | Optional capture-mode hint (max 20 chars when present) |
|
||||
| `CreatedAt` | `DateTime` | UTC creation timestamp set by the service on insert |
|
||||
|
||||
## Internal Logic
|
||||
Plain POCO; no behaviour. Identity is assigned by the database on insert (`InsertWithInt32IdentityAsync`).
|
||||
|
||||
## Dependencies
|
||||
None (no `using` directives on `Azaion.Services` / external libs).
|
||||
|
||||
## Consumers
|
||||
- `Azaion.Services.DetectionClassService` — CRUD operations
|
||||
- `AzaionDb.DetectionClasses` — linq2db table mapping (see `common_database_azaion_db.md`)
|
||||
- `Azaion.AdminApi.Program` — `POST/PATCH/DELETE /classes` endpoints
|
||||
|
||||
## Data Models
|
||||
Maps 1:1 to the `detection_classes` PostgreSQL table.
|
||||
|
||||
## Configuration
|
||||
None.
|
||||
|
||||
## External Integrations
|
||||
None directly; persisted via `IDbFactory` → PostgreSQL.
|
||||
|
||||
## Security
|
||||
Data is operator-controlled metadata; no PII or secrets.
|
||||
|
||||
## Tests
|
||||
- `e2e/Azaion.E2E/Tests/DetectionClassesTests.cs` — covers AZ-513 ACs 1–9
|
||||
@@ -0,0 +1,51 @@
|
||||
# Module: Azaion.Common.Requests.CreateDetectionClassRequest
|
||||
|
||||
## Purpose
|
||||
Request DTO + FluentValidation validator for `POST /classes` (AZ-513).
|
||||
|
||||
> **Cycle 1 (2026-05-13) origin** — added by AZ-513.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### CreateDetectionClassRequest
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Name` | `string` | Full display name |
|
||||
| `ShortName` | `string` | Short label |
|
||||
| `Color` | `string` | UI color string (hex or named) |
|
||||
| `MaxSizeM` | `double` | Max real-world size in meters |
|
||||
| `PhotoMode` | `string?` | Optional capture-mode hint |
|
||||
|
||||
### CreateDetectionClassValidator
|
||||
| Rule | Constraint |
|
||||
|------|-----------|
|
||||
| `Name` | NotEmpty, ≤ 120 chars |
|
||||
| `ShortName` | NotEmpty, ≤ 20 chars |
|
||||
| `Color` | NotEmpty, ≤ 20 chars |
|
||||
| `MaxSizeM` | > 0 |
|
||||
| `PhotoMode` | ≤ 20 chars when present |
|
||||
|
||||
## Internal Logic
|
||||
Plain DTO; validator runs in the `/classes` POST handler before the service call. Validation failures are surfaced via `Results.ValidationProblem(...)` (HTTP 400).
|
||||
|
||||
## Dependencies
|
||||
- FluentValidation
|
||||
|
||||
## Consumers
|
||||
- `Azaion.AdminApi.Program` `POST /classes`
|
||||
- `Azaion.Services.DetectionClassService.Create`
|
||||
|
||||
## Data Models
|
||||
Maps to the writable subset of `DetectionClass` (see `common_entities_detection_class.md`).
|
||||
|
||||
## Configuration
|
||||
None.
|
||||
|
||||
## External Integrations
|
||||
None.
|
||||
|
||||
## Security
|
||||
ApiAdmin-only endpoint; FluentValidation enforces field bounds. No HTML/JS sanitisation — the UI is responsible for safe rendering of `Name`, `ShortName`, `Color`.
|
||||
|
||||
## Tests
|
||||
- e2e: `AC1_Post_classes_creates_class_with_assigned_id`, `AC2_Post_classes_*`
|
||||
@@ -1,27 +1,22 @@
|
||||
# Module: Azaion.Common.Requests.GetResourceRequest
|
||||
|
||||
## Purpose
|
||||
Request DTOs and validator for resource access endpoints. Contains both `GetResourceRequest` and `CheckResourceRequest`.
|
||||
Request DTO and validator for the `POST /resources/get/{dataFolder?}` endpoint. The user's password is supplied per-request so the server can derive the per-user AES encryption key for the response stream.
|
||||
|
||||
> **Cycle 1 (2026-05-13) note** — the `Hardware` property and its `BadHardware` validator rule were removed by AZ-197 (admin-side hardware-binding cleanup). The wire-compat policy was "drop entirely" — any client still sending `Hardware` will not see it deserialized. The companion `CheckResourceRequest` was removed along with the `POST /resources/check` endpoint. See `_docs/03_implementation/batch_06_report.md`.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### CheckResourceRequest
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Hardware` | `string` | Hardware fingerprint to validate |
|
||||
|
||||
### GetResourceRequest
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Password` | `string` | User's password (used to derive encryption key) |
|
||||
| `Hardware` | `string` | Hardware fingerprint for authorization |
|
||||
| `Password` | `string` | User's password (used to derive the encryption key) |
|
||||
| `FileName` | `string` | Resource file to retrieve |
|
||||
|
||||
### GetResourceRequestValidator
|
||||
| Rule | Constraint | Error Code |
|
||||
|------|-----------|------------|
|
||||
| `Password` min length | >= 8 chars | `PasswordLengthIncorrect` |
|
||||
| `Hardware` not empty | Required | `BadHardware` |
|
||||
| `FileName` not empty | Required | `WrongResourceName` |
|
||||
|
||||
## Internal Logic
|
||||
@@ -32,7 +27,7 @@ Validator uses `BusinessException.GetMessage()` to derive user-facing error mess
|
||||
- FluentValidation
|
||||
|
||||
## Consumers
|
||||
- `Program.cs` `/resources/get/{dataFolder?}` and `/resources/check` endpoints
|
||||
- `Program.cs` `POST /resources/get/{dataFolder?}` endpoint
|
||||
|
||||
## Data Models
|
||||
None.
|
||||
@@ -44,7 +39,8 @@ None.
|
||||
None.
|
||||
|
||||
## Security
|
||||
Password is sent in the POST body (not URL) to avoid logging in access logs. Hardware fingerprint validates device authorization.
|
||||
- Password is sent in the POST body (not URL) to avoid logging in access logs.
|
||||
- Per-user encryption key derivation now uses `email + password` only (see `services_security.md`).
|
||||
|
||||
## Tests
|
||||
None.
|
||||
- `e2e/Azaion.E2E/Tests/ResourceTests.cs` (encrypted download / round-trip) — updated by AZ-197 to stop sending `Hardware`
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Module: Azaion.Common.Requests.RegisterDeviceResponse
|
||||
|
||||
## Purpose
|
||||
Response DTO returned by `POST /devices` (AZ-196) — provides the provisioning script with the freshly-generated `Serial`, `Email`, and one-shot plaintext `Password` for a new CompanionPC device user.
|
||||
|
||||
> **Cycle 1 (2026-05-13) origin** — added by AZ-196.
|
||||
|
||||
## Public Interface
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Serial` | `string` | Server-assigned device serial in the form `azj-NNNN` (zero-padded to 4 digits) |
|
||||
| `Email` | `string` | `{Serial}@azaion.com` — the persisted user's login email |
|
||||
| `Password` | `string` | Plaintext 32-char hex password — exposed exactly once at provisioning; never re-derivable from the SHA-384 hash that is persisted |
|
||||
|
||||
## Internal Logic
|
||||
Plain POCO. All field values are produced inside `UserService.RegisterDevice` (see `services_user_service.md`).
|
||||
|
||||
## Dependencies
|
||||
None.
|
||||
|
||||
## Consumers
|
||||
- `Azaion.AdminApi.Program` `POST /devices` (returned via `Results.Ok(...)` implicit)
|
||||
- `Azaion.Services.UserService.RegisterDevice` (constructs and returns the response)
|
||||
- Provisioning script (out-of-tree) — embeds the values into `device.conf` on the Jetson
|
||||
|
||||
## Data Models
|
||||
Mirrors a subset of fields written into the `users` row (`Email`, `PasswordHash`).
|
||||
|
||||
## Configuration
|
||||
None.
|
||||
|
||||
## External Integrations
|
||||
None.
|
||||
|
||||
## Security
|
||||
- The `Password` is the only chance to capture the plaintext — once the response is consumed by the provisioning pipeline, the value cannot be recovered from the database (only the SHA-384 hash is persisted).
|
||||
- The endpoint is gated by `apiAdminPolicy`. Treat the response as a credential — log carefully.
|
||||
|
||||
## Tests
|
||||
- e2e: `AC1_Post_devices_returns_serial_email_and_password`, `AC3_Returned_credentials_can_login`
|
||||
@@ -1,39 +0,0 @@
|
||||
# Module: Azaion.Common.Requests.SetHWRequest
|
||||
|
||||
## Purpose
|
||||
Request DTO and validator for setting a user's hardware fingerprint (`PUT /users/hardware/set`).
|
||||
|
||||
## Public Interface
|
||||
|
||||
### SetHWRequest
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Email` | `string` | Target user's email |
|
||||
| `Hardware` | `string?` | Hardware fingerprint (null clears it) |
|
||||
|
||||
### SetHWRequestValidator
|
||||
| Rule | Constraint | Error Code |
|
||||
|------|-----------|------------|
|
||||
| `Email` not empty | Required | `EmailLengthIncorrect` |
|
||||
|
||||
## Dependencies
|
||||
- `BusinessException`, `ExceptionEnum`
|
||||
- FluentValidation
|
||||
|
||||
## Consumers
|
||||
- `Program.cs` `/users/hardware/set` endpoint
|
||||
|
||||
## Data Models
|
||||
None.
|
||||
|
||||
## Configuration
|
||||
None.
|
||||
|
||||
## External Integrations
|
||||
None.
|
||||
|
||||
## Security
|
||||
None.
|
||||
|
||||
## Tests
|
||||
None.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Module: Azaion.Common.Requests.UpdateDetectionClassRequest
|
||||
|
||||
## Purpose
|
||||
Request DTO + FluentValidation validator for `PATCH /classes/{id}` (AZ-513). All fields are nullable so callers may send the complete body OR only the changed fields — the service applies partial-merge semantics.
|
||||
|
||||
> **Cycle 1 (2026-05-13) origin** — added by AZ-513.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### UpdateDetectionClassRequest
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Name` | `string?` | If non-null, replace existing |
|
||||
| `ShortName` | `string?` | If non-null, replace existing |
|
||||
| `Color` | `string?` | If non-null, replace existing |
|
||||
| `MaxSizeM` | `double?` | If non-null, replace existing |
|
||||
| `PhotoMode` | `string?` | If non-null, replace existing |
|
||||
|
||||
### UpdateDetectionClassValidator
|
||||
| Rule | Constraint (only checked when field is non-null) |
|
||||
|------|--------------------------------------------------|
|
||||
| `Name` | NotEmpty, ≤ 120 chars |
|
||||
| `ShortName` | NotEmpty, ≤ 20 chars |
|
||||
| `Color` | NotEmpty, ≤ 20 chars |
|
||||
| `MaxSizeM` | > 0 |
|
||||
| `PhotoMode` | ≤ 20 chars |
|
||||
|
||||
## Internal Logic
|
||||
Each rule is gated by `.When(r => r.Field != null)` — fields the caller did not send pass validation untouched. The service then applies the same null-check pattern when writing back.
|
||||
|
||||
## Dependencies
|
||||
- FluentValidation
|
||||
|
||||
## Consumers
|
||||
- `Azaion.AdminApi.Program` `PATCH /classes/{id:int}`
|
||||
- `Azaion.Services.DetectionClassService.Update`
|
||||
|
||||
## Data Models
|
||||
Optional / partial view over `DetectionClass`.
|
||||
|
||||
## Configuration
|
||||
None.
|
||||
|
||||
## External Integrations
|
||||
None.
|
||||
|
||||
## Security
|
||||
ApiAdmin-only endpoint. Per the AZ-513 spec, the UI sends the complete body on edit even though partial-merge is supported on the server — that keeps the implementer free to choose either policy without breaking the client.
|
||||
|
||||
## Tests
|
||||
- e2e: `AC3_Patch_classes_full_body_updates_class`, `AC4_Patch_classes_partial_body_only_updates_specified_field`, `AC5_Patch_classes_unknown_id_returns_404`, `AC6_Patch_classes_without_jwt_returns_401`
|
||||
@@ -0,0 +1,47 @@
|
||||
# Module: Azaion.Services.DetectionClassService
|
||||
|
||||
## Purpose
|
||||
CRUD service for `DetectionClass` rows backing the admin Detection Classes table. Wraps `IDbFactory.RunAdmin` calls and translates request DTOs into entity writes.
|
||||
|
||||
> **Cycle 1 (2026-05-13) origin** — added by AZ-513.
|
||||
|
||||
## Public Interface
|
||||
|
||||
### IDetectionClassService
|
||||
| Method | Signature | Description |
|
||||
|--------|-----------|-------------|
|
||||
| `Create` | `Task<DetectionClass> Create(CreateDetectionClassRequest request, CancellationToken ct)` | Inserts a new class; returns the entity with the DB-assigned `Id` |
|
||||
| `Update` | `Task<DetectionClass?> Update(int id, UpdateDetectionClassRequest request, CancellationToken ct)` | Partial-merge update; returns `null` when the id doesn't exist |
|
||||
| `Delete` | `Task<bool> Delete(int id, CancellationToken ct)` | Returns `true` when at least one row was deleted; `false` when the id wasn't present |
|
||||
|
||||
## Internal Logic
|
||||
- **Create**: instantiates `DetectionClass`, sets `CreatedAt = DateTime.UtcNow`, calls `db.InsertWithInt32IdentityAsync`, assigns the returned id back to the entity, returns it.
|
||||
- **Update**: loads the row by id under the admin connection, returns `null` if missing. Otherwise applies a null-aware merge: each non-null property on the request overwrites the entity, then `db.UpdateAsync(existing)` persists the row. The route returns 404 when the service returns null.
|
||||
- **Delete**: `db.DetectionClasses.DeleteAsync(x => x.Id == id, ct)`; returns `deleted > 0`. The route returns 404 when the service returns false.
|
||||
|
||||
All writes go through `IDbFactory.RunAdmin` (admin DB connection / role).
|
||||
|
||||
## Dependencies
|
||||
- `IDbFactory` (`Azaion.Common.Database.IDbFactory`)
|
||||
- `DetectionClass` entity
|
||||
- `CreateDetectionClassRequest`, `UpdateDetectionClassRequest`
|
||||
- `LinqToDB` extension methods (`FirstOrDefaultAsync`, `InsertWithInt32IdentityAsync`, `UpdateAsync`, `DeleteAsync`)
|
||||
|
||||
## Consumers
|
||||
- `Azaion.AdminApi.Program` — `POST /classes`, `PATCH /classes/{id:int}`, `DELETE /classes/{id:int}` handlers
|
||||
|
||||
## Data Models
|
||||
Operates on `DetectionClass` via `AzaionDb.DetectionClasses`.
|
||||
|
||||
## Configuration
|
||||
None.
|
||||
|
||||
## External Integrations
|
||||
PostgreSQL via `IDbFactory.RunAdmin`.
|
||||
|
||||
## Security
|
||||
- All endpoints that delegate to this service require `apiAdminPolicy` at the route level.
|
||||
- Validators run before the service (no extra defensive validation inside the service).
|
||||
|
||||
## Tests
|
||||
- `e2e/Azaion.E2E/Tests/DetectionClassesTests.cs` — covers AZ-513 ACs 1–9
|
||||
@@ -41,9 +41,9 @@ Uses `ResourcesConfig` (ResourcesFolder, SuiteInstallerFolder, SuiteStageInstall
|
||||
Local filesystem for resource storage.
|
||||
|
||||
## Security
|
||||
- Resources are encrypted per-user using a key derived from email + password + hardware hash
|
||||
- File deletion overwrites existing files before writing new ones
|
||||
- No path traversal protection on `dataFolder` parameter
|
||||
- Resources are encrypted per-user using a key derived from `email + password` (the hardware-hash component was removed by AZ-197 — see `services_security.md`).
|
||||
- File deletion overwrites existing files before writing new ones.
|
||||
- No path traversal protection on `dataFolder` parameter.
|
||||
|
||||
## Tests
|
||||
None.
|
||||
None at the module level. End-to-end coverage lives in `e2e/Azaion.E2E/Tests/ResourceTests.cs` (encrypted download / round-trip / 200 MB upload limit) — updated by AZ-197 to stop sending the `Hardware` field.
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
# Module: Azaion.Services.Security
|
||||
|
||||
## Purpose
|
||||
Static utility class providing cryptographic operations: password hashing, hardware fingerprint hashing, encryption key derivation, and AES-CBC stream encryption/decryption.
|
||||
Static utility class providing cryptographic operations: password hashing, encryption key derivation, and AES-CBC stream encryption/decryption.
|
||||
|
||||
> **Cycle 1 (2026-05-13) note** — `GetHWHash` was deleted and `GetApiEncryptionKey` was simplified from `(email, password, hardwareHash)` to `(email, password)` by AZ-197 (admin-side hardware-binding cleanup). The hardware-hash component of the derived key is gone; existing ciphertexts produced under the old derivation are no longer re-derivable from the new signature. See `_docs/03_implementation/batch_06_report.md`.
|
||||
|
||||
## Public Interface
|
||||
|
||||
| Method | Signature | Description |
|
||||
|--------|-----------|-------------|
|
||||
| `ToHash` | `static string ToHash(this string str)` | Extension: SHA-384 hash of input, returned as Base64 |
|
||||
| `GetHWHash` | `static string GetHWHash(string hardware)` | Derives a salted hash from hardware fingerprint string |
|
||||
| `GetApiEncryptionKey` | `static string GetApiEncryptionKey(string email, string password, string? hardwareHash)` | Derives an AES encryption key from email + password + hardware hash |
|
||||
| `GetApiEncryptionKey` | `static string GetApiEncryptionKey(string email, string password)` | Derives the per-user AES encryption key string from email + password (+ static salt) |
|
||||
| `EncryptTo` | `static async Task EncryptTo(this Stream inputStream, Stream toStream, string key, CancellationToken ct)` | AES-256-CBC encrypts a stream; prepends IV to output |
|
||||
| `DecryptTo` | `static async Task DecryptTo(this Stream encryptedStream, Stream toStream, string key, CancellationToken ct)` | Reads IV prefix, then AES-256-CBC decrypts stream |
|
||||
|
||||
## Internal Logic
|
||||
- **Password hashing**: `ToHash` uses SHA-384 with UTF-8 encoding, outputting Base64.
|
||||
- **Hardware hashing**: `GetHWHash` salts the raw hardware string with `"Azaion_{hardware}_%$$$)0_"` before hashing.
|
||||
- **Encryption key derivation**: `GetApiEncryptionKey` concatenates email, password, and hardware hash with a static salt, then hashes.
|
||||
- **Encryption key derivation**: `GetApiEncryptionKey` concatenates email and password with the static salt `"-#%@AzaionKey@%#---"`, then hashes via `ToHash` (SHA-384, Base64).
|
||||
- **Encryption**: AES-256-CBC with PKCS7 padding. Key is SHA-256 of the derived key string. IV is randomly generated and prepended to the output stream. Uses 512 KB buffer for streaming.
|
||||
- **Decryption**: Reads the first 16 bytes as IV, then AES-256-CBC decrypts with PKCS7 padding.
|
||||
|
||||
@@ -25,10 +25,9 @@ Static utility class providing cryptographic operations: password hashing, hardw
|
||||
- `System.Text.Encoding`
|
||||
|
||||
## Consumers
|
||||
- `UserService.CheckHardwareHash` — calls `GetHWHash` to verify hardware fingerprint
|
||||
- `Program.cs` `/resources/get` endpoint — calls `GetApiEncryptionKey`
|
||||
- `Program.cs` `/resources/get/{dataFolder}` endpoint — calls `GetApiEncryptionKey(user.Email, request.Password)`
|
||||
- `ResourcesService.GetEncryptedResource` — uses `EncryptTo` extension
|
||||
- `SecurityTest` — directly tests `GetApiEncryptionKey`, `EncryptTo`, `DecryptTo`
|
||||
- `Azaion.Test/SecurityTest` — directly tests `EncryptTo` / `DecryptTo` round-trips (no longer tests hardware-hash derivation)
|
||||
|
||||
## Data Models
|
||||
None.
|
||||
@@ -41,11 +40,11 @@ None.
|
||||
|
||||
## Security
|
||||
Core cryptographic module. Key observations:
|
||||
- Passwords are hashed with SHA-384 (no per-user salt, no key stretching — not bcrypt/scrypt/argon2)
|
||||
- Hardware hash uses a static salt
|
||||
- AES encryption uses SHA-256 of the derived key, with random IV per encryption
|
||||
- All salts/prefixes are hardcoded constants
|
||||
- Passwords are hashed with SHA-384 (no per-user salt, no key stretching — not bcrypt/scrypt/argon2). This is unchanged by AZ-197.
|
||||
- AES encryption uses SHA-256 of the derived key, with random IV per encryption.
|
||||
- All salts/prefixes are hardcoded constants.
|
||||
- Per AZ-197: device hardware fingerprints no longer participate in key derivation. The threat that hardware binding mitigated (credential reuse via desktop installers) was eliminated by the architectural shift to fTPM-secured Jetsons + browser-only SaaS access.
|
||||
|
||||
## Tests
|
||||
- `SecurityTest.EncryptDecryptTest` — round-trip encrypt/decrypt of a string
|
||||
- `SecurityTest.EncryptDecryptLargeFileTest` — round-trip encrypt/decrypt of a ~400 MB generated file
|
||||
- `Azaion.Test/SecurityTest.EncryptDecryptTest` — round-trip encrypt/decrypt of a string
|
||||
- `Azaion.Test/SecurityTest.EncryptDecryptLargeFileTest` — round-trip encrypt/decrypt of a ~400 MB generated file
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# Module: Azaion.Services.UserService
|
||||
|
||||
## Purpose
|
||||
Core business logic for user management: registration, authentication, hardware binding, role management, and account lifecycle.
|
||||
Core business logic for user management: registration (web users + provisioned devices), authentication, role management, and account lifecycle.
|
||||
|
||||
> **Cycle 1 (2026-05-13) note** — hardware-binding methods (`UpdateHardware`, `CheckHardwareHash`, private `UpdateLastLoginDate`) and the bound `IUserService` declarations were removed by AZ-197 (admin-side hardware-binding cleanup). Device auto-provisioning (`RegisterDevice`) was added by AZ-196. **Post-cycle-1 (security audit F-3)**: `RegisterDevice` was refactored to delegate the row insert to `RegisterUser`, and `RegisterUser` itself now relies on the new `users_email_uidx` UNIQUE INDEX (`env/db/06_users_email_unique.sql`) — the check-then-insert race is gone; `Npgsql.PostgresException(SqlState=23505)` is translated to `BusinessException(EmailExists)`. See `_docs/03_implementation/batch_05_report.md` and `batch_06_report.md`.
|
||||
|
||||
## Public Interface
|
||||
|
||||
@@ -9,43 +11,45 @@ Core business logic for user management: registration, authentication, hardware
|
||||
| Method | Signature | Description |
|
||||
|--------|-----------|-------------|
|
||||
| `RegisterUser` | `Task RegisterUser(RegisterUserRequest request, CancellationToken ct)` | Creates a new user with hashed password |
|
||||
| `ValidateUser` | `Task<User> ValidateUser(LoginRequest request, CancellationToken ct)` | Validates email + password, returns user |
|
||||
| `RegisterDevice` | `Task<RegisterDeviceResponse> RegisterDevice(CancellationToken ct)` | Creates a new `CompanionPC` user with auto-assigned `azj-NNNN` serial / email and a 32-char hex password (returned plaintext exactly once) |
|
||||
| `ValidateUser` | `Task<User> ValidateUser(LoginRequest request, CancellationToken ct)` | Validates email + password, returns user. Throws `NoEmailFound`, `WrongPassword`, or `UserDisabled` |
|
||||
| `GetByEmail` | `Task<User?> GetByEmail(string? email, CancellationToken ct)` | Cached user lookup by email |
|
||||
| `UpdateHardware` | `Task UpdateHardware(string email, string? hardware, CancellationToken ct)` | Sets/clears user's hardware fingerprint |
|
||||
| `UpdateQueueOffsets` | `Task UpdateQueueOffsets(string email, UserQueueOffsets offsets, CancellationToken ct)` | Updates user's annotation queue offsets |
|
||||
| `GetUsers` | `Task<IEnumerable<User>> GetUsers(string? searchEmail, RoleEnum? searchRole, CancellationToken ct)` | Lists users with optional email/role filters |
|
||||
| `CheckHardwareHash` | `Task<string> CheckHardwareHash(User user, string hardware, CancellationToken ct)` | Validates or initializes hardware binding |
|
||||
| `ChangeRole` | `Task ChangeRole(string email, RoleEnum newRole, CancellationToken ct)` | Changes a user's role |
|
||||
| `SetEnableStatus` | `Task SetEnableStatus(string email, bool isEnabled, CancellationToken ct)` | Enables or disables a user account |
|
||||
| `RemoveUser` | `Task RemoveUser(string email, CancellationToken ct)` | Permanently deletes a user |
|
||||
|
||||
## Internal Logic
|
||||
- **RegisterUser**: checks for duplicate email, hashes password via `Security.ToHash`, inserts via `RunAdmin`.
|
||||
- **ValidateUser**: finds user by email, compares password hash. Throws `NoEmailFound` or `WrongPassword`.
|
||||
- **RegisterUser**: hashes password via `Security.ToHash`, inserts via `RunAdmin`. Catches `Npgsql.PostgresException` with `SqlState == PostgresErrorCodes.UniqueViolation` (23505) on the `users_email_uidx` UNIQUE INDEX and rethrows as `BusinessException(EmailExists)`. The previous check-then-insert pattern was removed (race-prone before the index existed; redundant after).
|
||||
- **RegisterDevice**: calls private `NextDeviceIdentity` (read-only) to compute the next `azj-NNNN` serial + matching email, generates a 32-char hex password from `RandomNumberGenerator.GetBytes(16)`, then delegates the row insert to `RegisterUser` (so any future change to user-creation policy applies here too). Returns `{Serial, Email, Password}` (plaintext password exposed exactly once at provisioning time). On a serial-allocation race, the second caller's insert hits the UNIQUE INDEX and surfaces `BusinessException(EmailExists)`; the caller can retry.
|
||||
- **NextDeviceIdentity** (private): queries the most recent `RoleEnum.CompanionPC` user via `dbFactory.Run` (read connection), parses the `azj-NNNN` suffix (chars `[SerialNumberStart, SerialNumberLength)` of the email, constants on the class), increments by 1, returns `(serial, email)`.
|
||||
- **ValidateUser**: finds user by email, compares password hash. Throws `NoEmailFound`, `WrongPassword`, or `UserDisabled`.
|
||||
- **GetByEmail**: uses `ICache.GetFromCacheAsync` with key `User.{email}`.
|
||||
- **CheckHardwareHash**: on first access (null hardware), stores the raw hardware string and returns the hash. On subsequent access, compares hashes. Throws `HardwareIdMismatch` on mismatch. Also updates `LastLogin` timestamp.
|
||||
- **UpdateHardware/UpdateQueueOffsets**: use `RunAdmin` for writes, then invalidate cache.
|
||||
- **UpdateQueueOffsets**: writes via `RunAdmin`, then invalidates the user cache.
|
||||
- **GetUsers**: uses `WhereIf` for optional filter predicates.
|
||||
|
||||
Private method:
|
||||
- `UpdateLastLoginDate` — updates `LastLogin` to `DateTime.UtcNow`.
|
||||
Private constants (device provisioning):
|
||||
- `DeviceEmailPrefix = "azj-"`, `DeviceEmailDomain = "@azaion.com"`, `SerialNumberStart = 4`, `SerialNumberLength = 4`, `DevicePasswordBytes = 16`.
|
||||
|
||||
## Dependencies
|
||||
- `IDbFactory` (database access)
|
||||
- `ICache` (user caching)
|
||||
- `Security` (hashing)
|
||||
- `Security` (hashing — `ToHash`)
|
||||
- `System.Security.Cryptography.RandomNumberGenerator` (device password entropy)
|
||||
- `Npgsql` (`PostgresException`, `PostgresErrorCodes.UniqueViolation` — used to translate UNIQUE-INDEX violations to `BusinessException(EmailExists)`)
|
||||
- `BusinessException` (domain errors)
|
||||
- `QueryableExtensions.WhereIf`
|
||||
- `User`, `UserConfig`, `UserQueueOffsets`, `RoleEnum`
|
||||
- `RegisterUserRequest`, `LoginRequest`
|
||||
- `RegisterUserRequest`, `LoginRequest`, `RegisterDeviceResponse`
|
||||
|
||||
## Consumers
|
||||
- `Program.cs` — all `/users/*` endpoints delegate to `IUserService`
|
||||
- `Program.cs` — `/users/*` endpoints delegate to `IUserService`
|
||||
- `Program.cs` — `POST /devices` calls `RegisterDevice` (added by AZ-196)
|
||||
- `AuthService.GetCurrentUser` — calls `GetByEmail`
|
||||
- `Program.cs` `/resources/get` — calls `CheckHardwareHash`
|
||||
|
||||
## Data Models
|
||||
Operates on `User` entity via `AzaionDb.Users` table.
|
||||
Operates on `User` entity via `AzaionDb.Users` table. The `User.Hardware` column is left in place (nullable, unused) per AZ-197 — see the entity doc.
|
||||
|
||||
## Configuration
|
||||
None.
|
||||
@@ -54,9 +58,10 @@ None.
|
||||
PostgreSQL via `IDbFactory`.
|
||||
|
||||
## Security
|
||||
- Passwords hashed with SHA-384 (via `Security.ToHash`) before storage
|
||||
- Hardware binding prevents resource access from unauthorized devices
|
||||
- Read operations use read-only DB connection; writes use admin connection
|
||||
- Passwords hashed with SHA-384 (via `Security.ToHash`) before storage.
|
||||
- Device passwords are returned plaintext to the caller exactly once at provisioning; the persisted form is the SHA-384 hash. The plaintext is never re-derivable.
|
||||
- Read operations use the read-only DB connection; writes use the admin connection.
|
||||
|
||||
## Tests
|
||||
- `UserServiceTest.CheckHardwareHashTest` — integration test against live database
|
||||
- `Azaion.Test/UserServiceTest.cs` — unit/integration tests against the live test database (hardware-binding tests removed by AZ-197)
|
||||
- `e2e/Azaion.E2E/Tests/DeviceTests.cs` — e2e for AZ-196 device-provisioning ACs
|
||||
|
||||
Reference in New Issue
Block a user