mirror of
https://github.com/azaion/admin.git
synced 2026-04-22 08:06:34 +00:00
[AZ-189] [AZ-190] [AZ-191] [AZ-192] [AZ-193] [AZ-194] [AZ-195] Add e2e blackbox test suite
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
# Acceptance Criteria
|
||||
|
||||
Derived from validation rules, test assertions, configuration limits, and health check patterns found in the codebase.
|
||||
|
||||
## Authentication
|
||||
|
||||
| # | Criterion | Threshold | Source |
|
||||
|---|-----------|-----------|--------|
|
||||
| AC-1 | Login with valid credentials returns a JWT token | Token is non-empty string | `Program.cs` `/login` endpoint |
|
||||
| AC-2 | Login with unknown email returns error code 10 | HTTP 409, `ErrorCode: 10` | `UserService.ValidateUser` throws `NoEmailFound` |
|
||||
| AC-3 | Login with wrong password returns error code 30 | HTTP 409, `ErrorCode: 30` | `UserService.ValidateUser` throws `WrongPassword` |
|
||||
| AC-4 | JWT token expires after configured hours | Token `exp` claim = now + `TokenLifetimeHours` | `AuthService.CreateToken`, default 4 hours |
|
||||
| AC-5 | JWT token contains user ID, email, and role claims | Claims: `NameIdentifier`, `Name`, `Role` | `AuthService.CreateToken` |
|
||||
|
||||
## User Management
|
||||
|
||||
| # | Criterion | Threshold | Source |
|
||||
|---|-----------|-----------|--------|
|
||||
| AC-6 | Registration rejects email < 8 characters | Validation error, code `EmailLengthIncorrect` | `RegisterUserValidator` |
|
||||
| AC-7 | Registration rejects invalid email format | Validation error, code `WrongEmail` | `RegisterUserValidator` |
|
||||
| AC-8 | Registration rejects password < 8 characters | Validation error, code `PasswordLengthIncorrect` | `RegisterUserValidator` |
|
||||
| AC-9 | Registration rejects duplicate email | HTTP 409, `ErrorCode: 20` (EmailExists) | `UserService.RegisterUser` |
|
||||
| AC-10 | Password is stored as SHA-384 hash, never plaintext | `PasswordHash` column contains Base64 of SHA-384 | `Security.ToHash()` |
|
||||
| AC-11 | User listing supports optional email and role filters | Filters applied via `WhereIf` | `UserService.GetUsers` |
|
||||
| AC-12 | Only ApiAdmin role can create, list, modify, or delete users | Endpoints require `apiAdminPolicy` | `Program.cs` authorization |
|
||||
|
||||
## Hardware Binding
|
||||
|
||||
| # | Criterion | Threshold | Source |
|
||||
|---|-----------|-----------|--------|
|
||||
| AC-13 | First hardware check stores the hardware fingerprint | `hardware` column updated from null to provided string | `UserService.CheckHardwareHash` |
|
||||
| AC-14 | Subsequent hardware checks compare hash of provided hardware against stored | Hash comparison via `Security.GetHWHash` | `UserService.CheckHardwareHash` |
|
||||
| AC-15 | Hardware mismatch returns error code 40 | HTTP 409, `ErrorCode: 40` (HardwareIdMismatch) | `UserService.CheckHardwareHash` |
|
||||
| AC-16 | Admin can reset hardware by setting it to null | `PUT /users/hardware/set` with null hardware | `UserService.UpdateHardware` |
|
||||
|
||||
## Resource Management
|
||||
|
||||
| # | Criterion | Threshold | Source |
|
||||
|---|-----------|-----------|--------|
|
||||
| AC-17 | File upload supports up to 200 MB | Kestrel `MaxRequestBodySize = 209715200` | `Program.cs` |
|
||||
| AC-18 | Uploaded file is saved to configured resource folder | File written to `ResourcesConfig.ResourcesFolder` | `ResourcesService.SaveResource` |
|
||||
| AC-19 | Resource download returns AES-256-CBC encrypted stream | Encryption via `Security.EncryptTo` | `ResourcesService.GetEncryptedResource` |
|
||||
| AC-20 | Encrypted resource can be decrypted with same key | Round-trip encrypt/decrypt preserves data | `SecurityTest.EncryptDecryptTest` ✓ |
|
||||
| AC-21 | Large files (hundreds of MB) can be encrypted/decrypted | Round-trip works for ~400 MB files | `SecurityTest.EncryptDecryptLargeFileTest` ✓ |
|
||||
| AC-22 | Missing file upload returns error code 60 | HTTP 409, `ErrorCode: 60` (NoFileProvided) | `ResourcesService.SaveResource` |
|
||||
| AC-23 | Installer download returns latest `AzaionSuite.Iterative*` file | Scans installer folder, returns first match | `ResourcesService.GetInstaller` |
|
||||
| AC-24 | Only ApiAdmin can clear resource folders | `POST /resources/clear` requires `apiAdminPolicy` | `Program.cs` |
|
||||
|
||||
## API Behavior
|
||||
|
||||
| # | Criterion | Threshold | Source |
|
||||
|---|-----------|-----------|--------|
|
||||
| AC-25 | Business errors return HTTP 409 with JSON `{ErrorCode, Message}` | Handled by `BusinessExceptionHandler` | `BusinessExceptionHandler.TryHandleAsync` |
|
||||
| AC-26 | Swagger UI available in Development environment | `app.UseSwagger()` conditional on `IsDevelopment` | `Program.cs` |
|
||||
| AC-27 | Root URL redirects to /swagger | URL rewrite rule | `Program.cs` |
|
||||
| AC-28 | CORS allows requests from admin.azaion.com | Origins: `https://admin.azaion.com`, `http://admin.azaion.com` | `Program.cs` |
|
||||
@@ -0,0 +1,77 @@
|
||||
# Input Data Parameters
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Table: `users`
|
||||
|
||||
| Column | Type | Nullable | Description |
|
||||
|--------|------|----------|-------------|
|
||||
| `id` | uuid | No | Primary key |
|
||||
| `email` | varchar(160) | No | User identifier |
|
||||
| `password_hash` | varchar(255) | No | SHA-384 hash (Base64) |
|
||||
| `hardware` | text | Yes | Raw hardware fingerprint |
|
||||
| `hardware_hash` | varchar(120) | Yes | Unused column (legacy) |
|
||||
| `role` | varchar(20) | No | Text enum: None, Operator, Validator, CompanionPC, Admin, ResourceUploader, ApiAdmin |
|
||||
| `user_config` | varchar(512) | Yes | JSON: `{ QueueOffsets: { AnnotationsOffset, AnnotationsConfirmOffset, AnnotationsCommandsOffset } }` |
|
||||
| `created_at` | timestamp | No | Default: `now()` |
|
||||
| `last_login` | timestamp | Yes | Updated on hardware check |
|
||||
| `is_enabled` | bool | No | Default: `true` |
|
||||
|
||||
## API Request Schemas
|
||||
|
||||
### POST /login
|
||||
```json
|
||||
{ "Email": "string", "Password": "string" }
|
||||
```
|
||||
|
||||
### POST /users
|
||||
```json
|
||||
{ "Email": "string", "Password": "string", "Role": "RoleEnum (int)" }
|
||||
```
|
||||
|
||||
### PUT /users/hardware/set
|
||||
```json
|
||||
{ "Email": "string", "Hardware": "string|null" }
|
||||
```
|
||||
|
||||
### PUT /users/queue-offsets/set
|
||||
```json
|
||||
{ "Email": "string", "Offsets": { "AnnotationsOffset": 0, "AnnotationsConfirmOffset": 0, "AnnotationsCommandsOffset": 0 } }
|
||||
```
|
||||
|
||||
### POST /resources/get/{dataFolder?}
|
||||
```json
|
||||
{ "Password": "string", "Hardware": "string", "FileName": "string" }
|
||||
```
|
||||
|
||||
### POST /resources/check
|
||||
```json
|
||||
{ "Hardware": "string" }
|
||||
```
|
||||
|
||||
### POST /resources/{dataFolder?}
|
||||
Multipart form data with `IFormFile` field.
|
||||
|
||||
## Configuration Sections
|
||||
|
||||
### ConnectionStrings
|
||||
```json
|
||||
{ "AzaionDb": "Host=...;Database=azaion;Username=azaion_reader;Password=...", "AzaionDbAdmin": "Host=...;Database=azaion;Username=azaion_admin;Password=..." }
|
||||
```
|
||||
|
||||
### JwtConfig
|
||||
```json
|
||||
{ "Issuer": "AzaionApi", "Audience": "Annotators/OrangePi/Admins", "Secret": "...", "TokenLifetimeHours": 4 }
|
||||
```
|
||||
|
||||
### ResourcesConfig
|
||||
```json
|
||||
{ "ResourcesFolder": "Content", "SuiteInstallerFolder": "suite", "SuiteStageInstallerFolder": "suite-stage" }
|
||||
```
|
||||
|
||||
## Resource Files
|
||||
|
||||
The system stores and serves:
|
||||
- **AI models and DLLs** — stored in `ResourcesFolder`, served encrypted per-user
|
||||
- **Production installers** — files matching `AzaionSuite.Iterative*` in `SuiteInstallerFolder`
|
||||
- **Staging installers** — files matching `AzaionSuite.Iterative*` in `SuiteStageInstallerFolder`
|
||||
@@ -0,0 +1,86 @@
|
||||
# Expected Results
|
||||
|
||||
Maps every input data item to its quantifiable expected result.
|
||||
Tests use this mapping to compare actual system output against known-correct answers.
|
||||
|
||||
## Result Format Legend
|
||||
|
||||
| Result Type | When to Use | Example |
|
||||
|-------------|-------------|---------|
|
||||
| Exact value | Output must match precisely | `status_code: 200`, `error_code: 10` |
|
||||
| Threshold | Output must exceed or stay below a limit | `latency < 500ms` |
|
||||
| Pattern match | Output must match a string/regex pattern | `body contains "token"` |
|
||||
| Schema match | Output structure must conform to a schema | `response matches { Token: string }` |
|
||||
|
||||
## Input → Expected Result Mapping
|
||||
|
||||
### Authentication
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 1 | `POST /login { "Email": "admin@azaion.com", "Password": "<valid>" }` | Valid credentials | HTTP 200, body: `{ "token": "<non-empty JWT>" }` | exact (status), pattern (token is non-empty string) | N/A | N/A |
|
||||
| 2 | `POST /login { "Email": "nonexistent@x.com", "Password": "any" }` | Unknown email | HTTP 409, body: `{ "ErrorCode": 10, "Message": "No such email found." }` | exact (status, ErrorCode, Message) | N/A | N/A |
|
||||
| 3 | `POST /login { "Email": "admin@azaion.com", "Password": "wrongpw" }` | Wrong password | HTTP 409, body: `{ "ErrorCode": 30, "Message": "Passwords do not match." }` | exact (status, ErrorCode, Message) | N/A | N/A |
|
||||
|
||||
### User Registration
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 4 | `POST /users { "Email": "newuser@azaion.com", "Password": "validpass123", "Role": 10 }` | Valid registration (ApiAdmin auth) | HTTP 200, user created in DB | exact (status) | N/A | N/A |
|
||||
| 5 | `POST /users { "Email": "short", "Password": "validpass123", "Role": 10 }` | Email too short (<8 chars) | HTTP 400, validation error with code `EmailLengthIncorrect` | exact (status), substring (error code) | N/A | N/A |
|
||||
| 6 | `POST /users { "Email": "notanemail", "Password": "validpass123", "Role": 10 }` | Invalid email format (>=8 chars but not email) | HTTP 400, validation error with code `WrongEmail` | exact (status), substring (error code) | N/A | N/A |
|
||||
| 7 | `POST /users { "Email": "valid@azaion.com", "Password": "short", "Role": 10 }` | Password too short (<8 chars) | HTTP 400, validation error with code `PasswordLengthIncorrect` | exact (status), substring (error code) | N/A | N/A |
|
||||
| 8 | `POST /users { "Email": "admin@azaion.com", "Password": "validpass123", "Role": 10 }` | Duplicate email | HTTP 409, body: `{ "ErrorCode": 20, "Message": "Email already exists." }` | exact (status, ErrorCode, Message) | N/A | N/A |
|
||||
| 9 | `POST /users` (no auth header) | Unauthorized registration attempt | HTTP 401 | exact (status) | N/A | N/A |
|
||||
| 10 | `POST /users` (Operator role token) | Non-admin registration attempt | HTTP 403 | exact (status) | N/A | N/A |
|
||||
|
||||
### User Retrieval
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 11 | `GET /users/current` (valid JWT) | Get current user | HTTP 200, body contains email matching JWT claims | exact (status), exact (email matches claim) | N/A | N/A |
|
||||
| 12 | `GET /users` (ApiAdmin JWT) | List all users | HTTP 200, body is array of User objects | exact (status), schema (array of users) | N/A | N/A |
|
||||
| 13 | `GET /users?searchEmail=admin` (ApiAdmin JWT) | Filter by email substring | HTTP 200, all returned users have "admin" in email | exact (status), substring (filter applied) | N/A | N/A |
|
||||
| 14 | `GET /users?searchRole=10` (ApiAdmin JWT) | Filter by Operator role | HTTP 200, all returned users have Role=Operator | exact (status, role filter) | N/A | N/A |
|
||||
|
||||
### Hardware Binding
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 15 | `POST /resources/check { "Hardware": "CPU: Test..." }` (user with no hardware) | First-time hardware binding | HTTP 200, body: `true`, user's hardware column updated in DB | exact (status, body) | N/A | N/A |
|
||||
| 16 | `POST /resources/check { "Hardware": "CPU: Test..." }` (same user, same hardware) | Repeat check with matching hardware | HTTP 200, body: `true` | exact (status, body) | N/A | N/A |
|
||||
| 17 | `POST /resources/check { "Hardware": "DIFFERENT_HW" }` (user with different stored hardware) | Hardware mismatch | HTTP 409, body: `{ "ErrorCode": 40, "Message": "Hardware mismatch!..." }` | exact (status, ErrorCode), substring (Message) | N/A | N/A |
|
||||
|
||||
### Resource Management
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 18 | `POST /resources (multipart: testfile.txt, 100 bytes)` | Upload small file | HTTP 200, file exists at ResourcesFolder/testfile.txt | exact (status), file exists | N/A | N/A |
|
||||
| 19 | `POST /resources/subfolder (multipart: testfile.txt)` | Upload to subfolder | HTTP 200, file exists at ResourcesFolder/subfolder/testfile.txt | exact (status), file exists | N/A | N/A |
|
||||
| 20 | `GET /resources/list` (authenticated) | List resources in root folder | HTTP 200, body is array of filenames including "testfile.txt" | exact (status), set_contains (filename) | N/A | N/A |
|
||||
| 21 | `POST /resources/get { "Password": "validpass123", "Hardware": "<matching>", "FileName": "testfile.txt" }` | Download encrypted resource | HTTP 200, content-type: `application/octet-stream`, body decrypts to original file content | exact (status, content-type), exact (decrypted content matches original) | N/A | N/A |
|
||||
| 22 | `POST /resources (no file)` | Upload with no file | HTTP 409, body: `{ "ErrorCode": 60 }` | exact (status, ErrorCode) | N/A | N/A |
|
||||
|
||||
### Encryption Round-Trip
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 23 | Plaintext string "Hello World..." + key derived from (email, password, hwHash) | Encrypt then decrypt | Decrypted output == original plaintext | exact (string equality) | N/A | N/A |
|
||||
| 24 | Large file (~400 MB) + key | Encrypt then decrypt large file | SHA-256 of decrypted file == SHA-256 of original | exact (hash equality) | N/A | N/A |
|
||||
|
||||
### User Lifecycle
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 25 | `PUT /users/test@azaion.com/set-role/50` (ApiAdmin JWT) | Change role to ResourceUploader | HTTP 200, user's role updated in DB to `ResourceUploader` | exact (status, DB role) | N/A | N/A |
|
||||
| 26 | `PUT /users/test@azaion.com/disable` (ApiAdmin JWT) | Disable user | HTTP 200, user's is_enabled=false in DB | exact (status, DB flag) | N/A | N/A |
|
||||
| 27 | `PUT /users/test@azaion.com/enable` (ApiAdmin JWT) | Enable user | HTTP 200, user's is_enabled=true in DB | exact (status, DB flag) | N/A | N/A |
|
||||
| 28 | `DELETE /users/test@azaion.com` (ApiAdmin JWT) | Delete user | HTTP 200, user no longer exists in DB | exact (status), exact (user gone) | N/A | N/A |
|
||||
|
||||
### API Behavior
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|---|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 29 | `GET /` | Root URL | HTTP 302 redirect to `/swagger` | exact (status 302, Location header contains "swagger") | N/A | N/A |
|
||||
| 30 | Any endpoint from non-allowed origin | CORS rejection | No `Access-Control-Allow-Origin` header in response | exact (header absent) | N/A | N/A |
|
||||
| 31 | `OPTIONS /login` from `https://admin.azaion.com` | CORS preflight from allowed origin | `Access-Control-Allow-Origin: https://admin.azaion.com` | exact (header value) | N/A | N/A |
|
||||
@@ -0,0 +1,38 @@
|
||||
# Problem Statement
|
||||
|
||||
## What is this system?
|
||||
|
||||
The Azaion Admin API is the backend management service for the Azaion Suite — a platform for AI-powered data annotation workflows. The suite includes desktop client software (annotators, dataset explorers) that must be securely distributed and controlled.
|
||||
|
||||
## What problem does it solve?
|
||||
|
||||
The Azaion platform needs to:
|
||||
|
||||
1. **Control who can use the software** — only registered, authorized users should access the annotation tools. Different users have different permission levels (operators can annotate, validators can review, admins can manage everything).
|
||||
|
||||
2. **Bind software to specific hardware** — prevent unauthorized copying or redistribution of proprietary software components (AI models, DLLs). Each user's resources must be tied to their specific physical machine.
|
||||
|
||||
3. **Securely distribute software updates** — deliver installers and resource files (AI models, DLLs) to authorized users, encrypted such that only the intended user on the intended hardware can use them.
|
||||
|
||||
4. **Manage the user base** — admins need to create accounts, assign roles, enable/disable users, reset hardware bindings, and track activity (last login).
|
||||
|
||||
5. **Support annotation queue coordination** — users participate in annotation queues and need to maintain per-user offset tracking to resume work across sessions.
|
||||
|
||||
## Who are the users?
|
||||
|
||||
| User Type | Role(s) | What They Do |
|
||||
|-----------|---------|-------------|
|
||||
| Annotators | Operator | Use the desktop client to annotate data; submit annotations to queues |
|
||||
| Validators | Validator | Review annotations from queues, explore datasets |
|
||||
| Companion PCs | CompanionPC | Automated annotation devices (e.g., OrangePi) |
|
||||
| Resource Uploaders | ResourceUploader | Upload DLLs and AI models to the server |
|
||||
| System Administrators | ApiAdmin | Full control: user management, resource management, all operations |
|
||||
|
||||
## How does it work at a high level?
|
||||
|
||||
1. An admin creates user accounts via the API (or admin web panel at admin.azaion.com)
|
||||
2. Users authenticate via email/password and receive a JWT token
|
||||
3. On first resource access, the client sends its hardware fingerprint, which is stored for the user
|
||||
4. When downloading resources, the API encrypts files using a key derived from the user's email, password, and hardware hash — only that specific user on that specific machine can decrypt
|
||||
5. Installers (production and staging) are distributed to authenticated users without per-user encryption
|
||||
6. Users maintain annotation queue offsets that persist across sessions
|
||||
@@ -0,0 +1,42 @@
|
||||
# Restrictions
|
||||
|
||||
## Software Constraints
|
||||
|
||||
| Constraint | Value | Source |
|
||||
|-----------|-------|--------|
|
||||
| Runtime | .NET 10.0 | All `.csproj` files target `net10.0` |
|
||||
| Database | PostgreSQL | `DbFactory` uses `UsePostgreSQL()`, Npgsql provider |
|
||||
| ORM | linq2db 5.4.1 | No Entity Framework, no migration framework |
|
||||
| Container base | `mcr.microsoft.com/dotnet/aspnet:10.0` | Dockerfile |
|
||||
| Build platform | ARM64 | Woodpecker CI pipeline labels `platform: arm64` |
|
||||
| Max upload size | 200 MB | Kestrel `MaxRequestBodySize = 209715200` |
|
||||
|
||||
## Environment Constraints
|
||||
|
||||
| Constraint | Value | Source |
|
||||
|-----------|-------|--------|
|
||||
| Target OS | Linux (Docker) | Dockerfile `DockerDefaultTargetOS=Linux` |
|
||||
| DB port | 4312 (non-standard) | `env/db/00_install.sh` |
|
||||
| CORS origins | `admin.azaion.com` (HTTP + HTTPS) | `Program.cs` CORS policy |
|
||||
| Secrets | Environment variables (`ASPNETCORE_*` prefix) | `env/api/env.ps1`, no secret manager |
|
||||
| Deployment model | Single container, no orchestration | `deploy.cmd`, Dockerfile |
|
||||
|
||||
## Operational Constraints
|
||||
|
||||
| Constraint | Value | Source |
|
||||
|-----------|-------|--------|
|
||||
| DB connection model | Two connections: reader + admin | `DbFactory` with `Run` / `RunAdmin` |
|
||||
| Schema management | Manual SQL scripts (no ORM migrations) | `env/db/*.sql` |
|
||||
| CI/CD | Build-only (no automated tests in pipeline) | `.woodpecker/build-arm.yml` |
|
||||
| Private registry | `docker.azaion.com` and `localhost:5000` (CI) | `deploy.cmd`, CI config |
|
||||
| File storage | Local server filesystem | `ResourcesConfig.ResourcesFolder` |
|
||||
|
||||
## Security Constraints
|
||||
|
||||
| Constraint | Value | Source |
|
||||
|-----------|-------|--------|
|
||||
| Authentication | JWT Bearer (HMAC-SHA256) | `Program.cs` auth config |
|
||||
| Token lifetime | 4 hours | `appsettings.json` JwtConfig |
|
||||
| Password hashing | SHA-384 (no per-user salt, no key stretching) | `Security.ToHash()` |
|
||||
| Resource encryption | AES-256-CBC per-user (key from email + password + HW hash) | `Security.GetApiEncryptionKey`, `Security.EncryptTo` |
|
||||
| Hardware binding | Single device per user, admin reset required | `UserService.CheckHardwareHash` |
|
||||
@@ -0,0 +1,73 @@
|
||||
# Security Approach
|
||||
|
||||
## Authentication
|
||||
|
||||
- **Mechanism**: JWT Bearer tokens
|
||||
- **Signing**: HMAC-SHA256 with symmetric key from `JwtConfig.Secret`
|
||||
- **Validation**: Issuer, Audience, Lifetime, Signing Key — all validated by ASP.NET Core middleware
|
||||
- **Token lifetime**: 4 hours (configurable via `JwtConfig.TokenLifetimeHours`)
|
||||
- **Token claims**: UserID (`NameIdentifier`), Email (`Name`), Role (`Role`)
|
||||
|
||||
## Authorization
|
||||
|
||||
- **Model**: Role-based access control (RBAC)
|
||||
- **Policies**:
|
||||
- `apiAdminPolicy` — requires `ApiAdmin` role (used on user CRUD + folder clear endpoints)
|
||||
- `apiUploaderPolicy` — requires `ResourceUploader` or `ApiAdmin` (defined but never applied — dead code)
|
||||
- General `[Authorize]` — any authenticated user (used on resource endpoints, queue offsets)
|
||||
|
||||
## Password Security
|
||||
|
||||
- **Hashing**: SHA-384 (`Security.ToHash`), Base64-encoded
|
||||
- **No per-user salt**: All passwords use the same hash function without individual salts
|
||||
- **No key stretching**: Not using bcrypt, scrypt, or Argon2
|
||||
- **Minimum length**: 8 characters (enforced by FluentValidation)
|
||||
|
||||
## Hardware Fingerprint Binding
|
||||
|
||||
- **Storage**: Raw hardware string stored in `users.hardware` column
|
||||
- **Comparison**: Hashed with static salt (`"Azaion_{hw}_%$$$)0_"`) via SHA-384
|
||||
- **First-use binding**: Hardware auto-stored on first resource check; no admin approval step
|
||||
- **Reset**: Admin can set hardware to null via `PUT /users/hardware/set`
|
||||
|
||||
## Resource Encryption
|
||||
|
||||
- **Algorithm**: AES-256-CBC with PKCS7 padding
|
||||
- **Key derivation**: SHA-256 of `"{email}-{password}-{hwHash}-#%@AzaionKey@%#---"`
|
||||
- **IV**: Randomly generated per encryption, prepended to ciphertext (first 16 bytes)
|
||||
- **Scope**: Applied at download time; files stored unencrypted on server
|
||||
- **Buffer size**: 512 KB streaming buffers
|
||||
|
||||
## Database Security
|
||||
|
||||
- **Connection separation**: Read-only (`azaion_reader`) and admin (`azaion_admin`) DB users
|
||||
- **Privileges**: Reader has SELECT only; admin has SELECT, INSERT, UPDATE, DELETE
|
||||
- **Port**: Non-standard port 4312
|
||||
|
||||
## Transport Security
|
||||
|
||||
- **CORS**: Restricted to `admin.azaion.com` (HTTP + HTTPS)
|
||||
- **HTTPS enforcement**: Not configured in code (assumed at reverse proxy level)
|
||||
|
||||
## Input Validation
|
||||
|
||||
- **Framework**: FluentValidation (auto-discovered validators)
|
||||
- **Validated requests**: RegisterUserRequest, GetResourceRequest, SetHWRequest
|
||||
- **Not validated**: LoginRequest, SetUserQueueOffsetsRequest, CheckResourceRequest (partial)
|
||||
|
||||
## Secrets Management
|
||||
|
||||
- **Method**: Environment variables with `ASPNETCORE_` prefix
|
||||
- **Sensitive values**: DB connection strings (passwords), JWT secret
|
||||
- **Not in source**: `appsettings.json` omits connection strings and JWT secret
|
||||
|
||||
## Known Security Observations
|
||||
|
||||
1. SHA-384 without per-user salt is vulnerable to rainbow table attacks
|
||||
2. `hardware_hash` DB column exists but is unused — application computes hashes at runtime
|
||||
3. No path traversal protection on `dataFolder` parameter in resource endpoints
|
||||
4. Test file contains hardcoded DB credentials for a remote server
|
||||
5. No rate limiting on login endpoint
|
||||
6. No audit trail for security-relevant operations (logins, role changes, user deletions)
|
||||
7. No HTTPS enforcement in application code
|
||||
8. Static encryption key salts are hardcoded in source code
|
||||
Reference in New Issue
Block a user