Multi-Tenancy
Tenant isolation with per-tenant keys, quota enforcement, and scoped middleware.
Trove provides built-in support for multi-tenant storage through tenant keys on data models, per-tenant quota enforcement, and middleware scoping. When deployed with the Forge extension, tenant identification is handled automatically via the X-Subject-ID header.
Tenant Key Model
Every storage model carries a TenantKey field that associates the record with a specific tenant:
Bucket Model
type Bucket struct {
ID string `json:"id"`
Name string `json:"name"`
TenantKey string `json:"tenant_key,omitempty"`
// ... other fields
}Object Model
type Object struct {
ID string `json:"id"`
BucketID string `json:"bucket_id"`
Key string `json:"key"`
TenantKey string `json:"tenant_key,omitempty"`
// ... other fields
}Quota Model
Quotas are tracked per-tenant with both byte and object count limits:
type Quota struct {
TenantKey string `json:"tenant_key"`
UsedBytes int64 `json:"used_bytes"`
LimitBytes int64 `json:"limit_bytes"`
ObjectCount int64 `json:"object_count"`
LimitObjects int64 `json:"limit_objects"`
UpdatedAt time.Time `json:"updated_at"`
}Per-Tenant Quota Enforcement
When a tenant's usage exceeds their configured limits, Put operations return trove.ErrQuotaExceeded:
// Upload fails when quota is exceeded
_, err := t.Put(ctx, "uploads", "large-file.bin", data)
if errors.Is(err, trove.ErrQuotaExceeded) {
// Tenant has exceeded their storage quota
http.Error(w, "Storage quota exceeded", http.StatusInsufficientStorage)
}Quota limits apply to both total bytes and total object count. The Forge extension enforces these limits automatically in the upload handlers.
Tenant Identification
With Forge Extension
When running inside Forge, the tenant is identified by the X-Subject-ID HTTP header, which is typically set by authentication middleware (e.g., from a JWT or API key).
PUT /api/trove/buckets/uploads/objects/report.pdf
X-Subject-ID: tenant-abc
Content-Type: application/pdfThe extension automatically stamps every created bucket and object with the subject ID as the TenantKey.
Standalone Mode
Without Forge, set the tenant context explicitly using the store's tenant-aware list options or by managing the TenantKey field directly when writing metadata:
// Filter objects by tenant when listing
objects, err := store.ListObjects(ctx, "uploads",
store.WithTenantKey("tenant-abc"),
)Middleware Scoping by Tenant
Use ScopeFunc to apply middleware only to specific tenants. This enables per-tenant encryption keys, compression settings, or scanning policies.
import "github.com/xraph/trove/middleware"
// Extract tenant from context
tenantScope := &middleware.ScopeFunc{
Fn: func(ctx context.Context, bucket, key string) bool {
tenantID := ctx.Value("tenant_id").(string)
return tenantID == "acme-corp"
},
Desc: "tenant(acme-corp)",
}
// Encrypt only Acme Corp's data with their key
t, _ := trove.Open(drv,
trove.WithScopedMiddleware(tenantScope,
encrypt.New(encrypt.WithKeyProvider(acmeKeyProvider)),
),
)Per-Tenant Encryption Keys
Combine ScopeFunc with a dynamic KeyProvider that returns different keys per tenant:
type TenantKeyProvider struct {
vault VaultClient
}
func (p *TenantKeyProvider) Key(ctx context.Context) ([]byte, error) {
tenantID := ctx.Value("tenant_id").(string)
return p.vault.GetKey(ctx, "trove/keys/"+tenantID)
}
t, _ := trove.Open(drv,
trove.WithMiddleware(
encrypt.New(encrypt.WithKeyProvider(&TenantKeyProvider{vault: vc})),
),
)Integration with Warden
When used alongside the Warden extension (Forge's authorization engine), Trove checks access policies before every operation. Warden policies can be tenant-scoped:
// Warden policy: only allow access to own tenant's buckets
// Policy evaluation happens in the Forge extension's middleware layer
// The X-Subject-ID header identifies the tenant
// Warden checks: does subject "tenant-abc" have permission to
// read from bucket "uploads" with tenant_key "tenant-abc"?This provides defense-in-depth: tenant isolation at the data model level (TenantKey filtering), the quota level (per-tenant limits), the middleware level (scoped transformations), and the authorization level (Warden policies).
Tenant Isolation Guarantees
| Layer | Mechanism | Prevents |
|---|---|---|
| Data model | TenantKey field on Bucket, Object, Quota | Cross-tenant data access |
| Quota | Per-tenant byte and object limits | One tenant consuming all storage |
| Middleware | ScopeFunc with tenant predicate | Applying wrong encryption keys |
| Authorization | Warden policy checks on X-Subject-ID | Unauthorized API access |
| Routing | Per-tenant backend routing | Co-mingling tenant data on disk |
Cross-Tenant Prevention
- Listing operations are scoped by
TenantKey-- tenants only see their own resources - There is no built-in API to query across tenants
- Admin or superuser access requires explicit bypass logic in your application layer
- Quota enforcement is per-tenant and cannot be circumvented by API calls