Files
server/util/Seeder/CLAUDE.md

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

216 lines
7.9 KiB
Markdown
Raw Permalink Normal View History

# Seeder - Claude Code Context
## Ubiquitous Language
The Seeder follows six core patterns:
1. **Factories** - Create ONE entity with encryption. Named `{Entity}Seeder` with `Create{Type}{Entity}()` methods. Do not interact with database.
2. **Recipes** - Orchestrate MANY entities. Named `{DomainConcept}Recipe`. **MUST have `Seed()` method** as primary interface, not `AddToOrganization()` or similar. Use parameters for variations, not separate methods. Compose Factories internally.
3. **Models** - DTOs bridging SDK ↔ Server format. Named `{Entity}ViewDto` (plaintext), `Encrypted{Entity}Dto` (SDK format). Pure data, no logic.
4. **Scenes** - Complete test scenarios with ID mangling. Implement `IScene<TReques, TResult>`. Async, returns `SceneResult<TResult>` with MangleMap and result property populated with `TResult`. Named `{Scenario}Scene`.
5. **Queries** - Read-only data retrieval. Implement `IQuery<TRequest, TResult>`. Synchronous, no DB modifications. Named `{DataToRetrieve}Query`.
6. **Data** - Static, filterable test data collections (Companies, Passwords, Names, OrgStructures). Deterministic, composable. Enums provide public API.
## The Recipe Contract
Recipes follow strict rules (like a cooking recipe that you follow completely):
1. A Recipe SHALL have exactly one public method named `Seed()`
2. A Recipe MUST produce one cohesive result (like baking one complete cake)
3. A Recipe MAY have overloaded `Seed()` methods with different parameters
4. A Recipe SHALL use private helper methods for internal steps
5. A Recipe SHALL use BulkCopy for performance when creating multiple entities
6. A Recipe SHALL compose Factories for individual entity creation
7. A Recipe SHALL NOT expose implementation details as public methods
**Current violations** (to be refactored):
- `CiphersRecipe` - Uses `AddLoginCiphersToOrganization()` instead of `Seed()`
- `CollectionsRecipe` - Uses `AddFromStructure()` and `AddToOrganization()` instead of `Seed()`
- `GroupsRecipe` - Uses `AddToOrganization()` instead of `Seed()`
- `OrganizationDomainRecipe` - Uses `AddVerifiedDomainToOrganization()` instead of `Seed()`
## Pattern Decision Tree
```
Need to create test data?
├─ ONE entity with encryption? → Factory
├─ MANY entities as cohesive operation? → Recipe
├─ Complete test scenario with ID mangling to be used by the Seeder API? → Scene
├─ READ existing seeded data? → Query
└─ Data transformation SDK ↔ Server? → Model
```
## When to Use the Seeder
✅ Use for:
- Local development database setup
- Integration test data creation
- Performance testing with realistic encrypted data
❌ Do NOT use for:
- Production data
- Copying real user vaults (use backup/restore instead)
## Zero-Knowledge Architecture
**Critical Principle:** Unencrypted vault data never leaves the client. The server never sees plaintext.
### Why Seeder Uses the Rust SDK
The Seeder must behave exactly like any other Bitwarden client. Since the server:
- Never receives plaintext
- Cannot perform encryption (doesn't have keys)
- Only stores/retrieves encrypted blobs
...the Seeder cannot simply write plaintext to the database. It must:
1. Generate encryption keys (like a client does during account setup)
2. Encrypt vault data client-side (using the same SDK the real clients use)
3. Store only the encrypted result
This is why we use the Rust SDK via FFI - it's the same cryptographic implementation used by the official clients.
## Cipher Encryption Architecture
### The Two-State Pattern
Bitwarden uses a clean separation between encrypted and decrypted data:
| State | SDK Type | Description | Stored in DB? |
| --------- | ------------ | ------------------------- | ------------- |
| Plaintext | `CipherView` | Decrypted, human-readable | Never |
| Encrypted | `Cipher` | EncString values | Yes |
**Encryption flow:**
```
CipherView (plaintext) → encrypt_composite() → Cipher (encrypted)
```
**Decryption flow:**
```
Cipher (encrypted) → decrypt() → CipherView (plaintext)
```
### SDK vs Server Format Difference
**Critical:** The SDK and server use different JSON structures.
**SDK Cipher (nested):**
```json
{
"name": "2.abc...",
"login": {
"username": "2.def...",
"password": "2.ghi..."
}
}
```
**Server Cipher.Data (flat CipherLoginData):**
```json
{
"Name": "2.abc...",
"Username": "2.def...",
"Password": "2.ghi..."
}
```
### Data Flow in Seeder
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ CipherViewDto │────▶│ Rust SDK │────▶│ EncryptedCipherDto │
│ (plaintext) │ │ encrypt_cipher │ │ (SDK Cipher) │
└─────────────────┘ └──────────────────┘ └─────────────────────┘
┌───────────────────────┐
│ TransformToServer │
│ (flatten nested → │
│ flat structure) │
└───────────────────────┘
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ Server Cipher │◀────│ CipherLoginData │◀────│ Flattened JSON │
│ Entity │ │ (serialized) │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────────┘
```
### Key Hierarchy
Bitwarden uses a two-level encryption hierarchy:
1. **User/Organization Key** - Encrypts the cipher's individual key
2. **Cipher Key** (optional) - Encrypts the actual cipher data
For seeding, we use the organization's symmetric key directly (no per-cipher key).
## Rust SDK FFI
### Error Handling
SDK functions return JSON with an `"error"` field on failure:
```json
{ "error": "Failed to parse CipherView JSON" }
```
Always check for `"error"` in the response before parsing.
## Testing
Integration tests in `test/SeederApi.IntegrationTest` verify:
1. **Roundtrip encryption** - Encrypt then decrypt preserves plaintext
2. **Server format compatibility** - Output matches CipherLoginData structure
3. **Field encryption** - Custom fields are properly encrypted
4. **Security** - Plaintext never appears in encrypted output
## Common Patterns
### Creating a Cipher
```csharp
var sdk = new RustSdkService();
var seeder = new CipherSeeder(sdk);
var cipher = seeder.CreateOrganizationLoginCipher(
organizationId,
orgKey, // Base64-encoded symmetric key
name: "My Login",
username: "user@example.com",
password: "secret123");
```
### Bulk Cipher Creation
```csharp
var recipe = new CiphersRecipe(dbContext, sdkService);
var cipherIds = recipe.AddLoginCiphersToOrganization(
organizationId,
orgKey,
collectionIds,
count: 100);
```
## Security Reminders
- Generated test passwords are intentionally weak (`asdfasdfasdf`)
- Never commit database dumps containing seeded data to version control
- Seeded keys are for testing only - regenerate for each test run