mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-28 07:44:56 +00:00
446 lines
13 KiB
Markdown
446 lines
13 KiB
Markdown
# Multi-Tenant System Implementation
|
|
|
|
Complete multi-tenant support for both blob storage and structured data with access control, quotas, and namespace isolation.
|
|
|
|
## Features
|
|
|
|
### Blob Storage (Files)
|
|
1. ✅ **Multi-tenant isolation** - Namespace-based data separation
|
|
2. ✅ **Access control** - Permission checks (read, write, delete)
|
|
3. ✅ **Storage quotas** - Size limits, file count limits, max file size
|
|
4. ✅ **Virtual root directories** - Scoped views, path sandboxing
|
|
|
|
### Structured Data (User/Page/KV/Lists)
|
|
1. ✅ **Multi-tenant isolation** - Tenant-scoped data access
|
|
2. ✅ **Access control** - Role-based permissions (owner, admin, member, viewer)
|
|
3. ✅ **Storage quotas** - Record count limits, data size limits, list length limits
|
|
|
|
## Architecture
|
|
|
|
### Components
|
|
|
|
#### 1. Tenant Context (`tenant-context.ts`)
|
|
- **TenantIdentity**: User identity within a tenant (role, permissions)
|
|
- **TenantQuota**: Resource limits and current usage
|
|
- **TenantContext**: Context for all operations (identity + quota + namespace)
|
|
- **TenantManager**: Manages tenants, quotas, and usage tracking
|
|
|
|
#### 2. Key-Value Store (`kv-store.ts`)
|
|
- **Primitive types**: string, number, boolean, null
|
|
- **Complex types**: objects, arrays/lists
|
|
- **Operations**: get, set, delete, exists
|
|
- **List operations**: listAdd, listGet, listRemove, listLength, listClear
|
|
- **Batch operations**: mget, mset
|
|
- **Query operations**: list, count, clear
|
|
|
|
#### 3. Tenant-Aware Blob Storage (`tenant-aware-storage.ts`)
|
|
- Wraps any BlobStorage implementation
|
|
- Automatic namespace scoping
|
|
- Permission checking on all operations
|
|
- Quota enforcement
|
|
- Usage tracking
|
|
|
|
## Usage Examples
|
|
|
|
### 1. Initialize Tenant System
|
|
|
|
```typescript
|
|
import {
|
|
InMemoryTenantManager,
|
|
InMemoryKVStore,
|
|
TenantAwareBlobStorage,
|
|
createBlobStorage
|
|
} from './dbal/ts/src'
|
|
|
|
// Create tenant manager
|
|
const tenantManager = new InMemoryTenantManager()
|
|
|
|
// Create tenant with quotas
|
|
await tenantManager.createTenant('tenant-123', {
|
|
maxBlobStorageBytes: 1024 * 1024 * 1024, // 1GB
|
|
maxBlobCount: 10000,
|
|
maxBlobSizeBytes: 100 * 1024 * 1024, // 100MB per file
|
|
maxRecords: 100000,
|
|
maxDataSizeBytes: 500 * 1024 * 1024, // 500MB
|
|
maxListLength: 10000
|
|
})
|
|
```
|
|
|
|
### 2. Blob Storage with Multi-Tenancy
|
|
|
|
```typescript
|
|
// Create base storage
|
|
const baseStorage = createBlobStorage({
|
|
type: 's3',
|
|
s3: { bucket: 'shared-bucket', region: 'us-east-1' }
|
|
})
|
|
|
|
// Wrap with tenant-aware storage
|
|
const tenantStorage = new TenantAwareBlobStorage(
|
|
baseStorage,
|
|
tenantManager,
|
|
'tenant-123',
|
|
'user-456'
|
|
)
|
|
|
|
// Upload - automatically scoped and quota-checked
|
|
await tenantStorage.upload('documents/report.pdf', pdfBuffer, {
|
|
contentType: 'application/pdf'
|
|
})
|
|
// Actual S3 key: tenants/tenant-123/documents/report.pdf
|
|
|
|
// List - scoped to tenant
|
|
const files = await tenantStorage.list({ prefix: 'documents/' })
|
|
// Only returns files under tenants/tenant-123/documents/
|
|
|
|
// Check usage
|
|
const stats = await tenantStorage.getStats()
|
|
console.log(`Using ${stats.totalSize} bytes, ${stats.count} files`)
|
|
```
|
|
|
|
### 3. Key-Value Store with Multi-Tenancy
|
|
|
|
```typescript
|
|
const kvStore = new InMemoryKVStore()
|
|
const context = await tenantManager.getTenantContext('tenant-123', 'user-456')
|
|
|
|
// Store primitives
|
|
await kvStore.set('config:theme', 'dark', context)
|
|
await kvStore.set('config:fontSize', 16, context)
|
|
await kvStore.set('config:enabled', true, context)
|
|
|
|
// Store objects
|
|
await kvStore.set('user:profile', {
|
|
name: 'John Doe',
|
|
email: 'john@example.com',
|
|
preferences: { notifications: true }
|
|
}, context)
|
|
|
|
// Retrieve
|
|
const theme = await kvStore.get('config:theme', context) // 'dark'
|
|
const profile = await kvStore.get('user:profile', context)
|
|
|
|
// Lists
|
|
await kvStore.listAdd('tasks', ['Task 1', 'Task 2'], context)
|
|
await kvStore.listAdd('tasks', ['Task 3'], context)
|
|
|
|
const tasks = await kvStore.listGet('tasks', context) // ['Task 1', 'Task 2', 'Task 3']
|
|
const length = await kvStore.listLength('tasks', context) // 3
|
|
|
|
await kvStore.listRemove('tasks', 'Task 2', context)
|
|
const updated = await kvStore.listGet('tasks', context) // ['Task 1', 'Task 3']
|
|
|
|
// TTL support
|
|
await kvStore.set('session:token', 'abc123', context, 3600) // Expires in 1 hour
|
|
|
|
// Batch operations
|
|
await kvStore.mset(new Map([
|
|
['key1', 'value1'],
|
|
['key2', 42],
|
|
['key3', true]
|
|
]), context)
|
|
|
|
const values = await kvStore.mget(['key1', 'key2', 'key3'], context)
|
|
```
|
|
|
|
### 4. Access Control
|
|
|
|
```typescript
|
|
// Set user roles
|
|
tenantManager.setUserRole('tenant-123', 'user-owner', 'owner')
|
|
tenantManager.setUserRole('tenant-123', 'user-admin', 'admin')
|
|
tenantManager.setUserRole('tenant-123', 'user-viewer', 'viewer')
|
|
|
|
// Owner/admin can do everything
|
|
const ownerContext = await tenantManager.getTenantContext('tenant-123', 'user-owner')
|
|
await kvStore.set('data', 'value', ownerContext) // ✅ Success
|
|
|
|
// Viewer can only read
|
|
const viewerContext = await tenantManager.getTenantContext('tenant-123', 'user-viewer')
|
|
await kvStore.get('data', viewerContext) // ✅ Success
|
|
await kvStore.set('data', 'new', viewerContext) // ❌ Permission denied (403)
|
|
|
|
// Custom permissions
|
|
tenantManager.grantPermission('tenant-123', 'user-custom', 'write:kv')
|
|
tenantManager.grantPermission('tenant-123', 'user-custom', 'read:blob')
|
|
```
|
|
|
|
### 5. Quota Management
|
|
|
|
```typescript
|
|
// Check current usage
|
|
const usage = await tenantManager.getUsage('tenant-123')
|
|
console.log({
|
|
blobStorage: `${usage.currentBlobStorageBytes} / ${usage.maxBlobStorageBytes}`,
|
|
blobCount: `${usage.currentBlobCount} / ${usage.maxBlobCount}`,
|
|
records: `${usage.currentRecords} / ${usage.maxRecords}`,
|
|
dataSize: `${usage.currentDataSizeBytes} / ${usage.maxDataSizeBytes}`
|
|
})
|
|
|
|
// Update quotas
|
|
await tenantManager.updateQuota('tenant-123', {
|
|
maxBlobStorageBytes: 2 * 1024 * 1024 * 1024, // Increase to 2GB
|
|
maxRecords: 200000 // Increase to 200k records
|
|
})
|
|
|
|
// Quota exceeded handling
|
|
try {
|
|
await tenantStorage.upload('large-file.bin', hugeBuffer)
|
|
} catch (error) {
|
|
if (error.code === 429) {
|
|
console.error('Quota exceeded:', error.message)
|
|
}
|
|
}
|
|
```
|
|
|
|
### 6. Multi-Tenant Patterns
|
|
|
|
#### Per-User Sandboxing
|
|
```typescript
|
|
function getUserStorage(userId: string) {
|
|
return new TenantAwareBlobStorage(
|
|
baseStorage,
|
|
tenantManager,
|
|
'app',
|
|
userId
|
|
)
|
|
}
|
|
|
|
const user1Storage = getUserStorage('user-1')
|
|
const user2Storage = getUserStorage('user-2')
|
|
|
|
// Isolated storage
|
|
await user1Storage.upload('avatar.jpg', data) // app/users/user-1/avatar.jpg
|
|
await user2Storage.upload('avatar.jpg', data) // app/users/user-2/avatar.jpg
|
|
```
|
|
|
|
#### Organization Hierarchy
|
|
```typescript
|
|
function getOrgStorage(orgId: string, teamId?: string) {
|
|
const tenantId = teamId ? `${orgId}:${teamId}` : orgId
|
|
return new TenantAwareBlobStorage(
|
|
baseStorage,
|
|
tenantManager,
|
|
tenantId,
|
|
'system'
|
|
)
|
|
}
|
|
|
|
const orgStorage = getOrgStorage('acme-corp')
|
|
const teamStorage = getOrgStorage('acme-corp', 'engineering')
|
|
```
|
|
|
|
#### Feature-Based Isolation
|
|
```typescript
|
|
// Separate storage for different features
|
|
const documentsStorage = new TenantAwareBlobStorage(
|
|
baseStorage,
|
|
tenantManager,
|
|
'tenant-123',
|
|
'documents-service'
|
|
)
|
|
|
|
const backupsStorage = new TenantAwareBlobStorage(
|
|
baseStorage,
|
|
tenantManager,
|
|
'tenant-123',
|
|
'backup-service'
|
|
)
|
|
```
|
|
|
|
## C++ Implementation
|
|
|
|
The C++ implementation mirrors the TypeScript design:
|
|
|
|
### Headers
|
|
|
|
```cpp
|
|
// dbal/cpp/include/dbal/tenant_context.hpp
|
|
namespace dbal {
|
|
namespace tenant {
|
|
|
|
struct TenantIdentity {
|
|
std::string tenantId;
|
|
std::string userId;
|
|
std::string role; // owner, admin, member, viewer
|
|
std::set<std::string> permissions;
|
|
};
|
|
|
|
struct TenantQuota {
|
|
// Blob storage quotas
|
|
std::optional<size_t> maxBlobStorageBytes;
|
|
std::optional<size_t> maxBlobCount;
|
|
std::optional<size_t> maxBlobSizeBytes;
|
|
|
|
// Structured data quotas
|
|
std::optional<size_t> maxRecords;
|
|
std::optional<size_t> maxDataSizeBytes;
|
|
std::optional<size_t> maxListLength;
|
|
|
|
// Current usage
|
|
size_t currentBlobStorageBytes;
|
|
size_t currentBlobCount;
|
|
size_t currentRecords;
|
|
size_t currentDataSizeBytes;
|
|
};
|
|
|
|
class TenantContext {
|
|
public:
|
|
TenantContext(const TenantIdentity& identity,
|
|
const TenantQuota& quota,
|
|
const std::string& ns);
|
|
|
|
bool canRead(const std::string& resource) const;
|
|
bool canWrite(const std::string& resource) const;
|
|
bool canDelete(const std::string& resource) const;
|
|
|
|
bool canUploadBlob(size_t sizeBytes) const;
|
|
bool canCreateRecord() const;
|
|
bool canAddToList(size_t additionalItems) const;
|
|
|
|
const TenantIdentity& identity() const { return identity_; }
|
|
TenantQuota& quota() { return quota_; }
|
|
const std::string& namespace_() const { return namespace__; }
|
|
|
|
private:
|
|
TenantIdentity identity_;
|
|
TenantQuota quota_;
|
|
std::string namespace__;
|
|
};
|
|
|
|
} // namespace tenant
|
|
} // namespace dbal
|
|
```
|
|
|
|
### Usage
|
|
|
|
```cpp
|
|
#include "dbal/tenant_context.hpp"
|
|
#include "dbal/blob_storage.hpp"
|
|
#include "dbal/kv_store.hpp"
|
|
|
|
using namespace dbal;
|
|
|
|
// Create tenant context
|
|
tenant::TenantIdentity identity{
|
|
.tenantId = "tenant-123",
|
|
.userId = "user-456",
|
|
.role = "admin",
|
|
.permissions = {"read:*", "write:*", "delete:*"}
|
|
};
|
|
|
|
tenant::TenantQuota quota{
|
|
.maxBlobStorageBytes = 1024ULL * 1024 * 1024, // 1GB
|
|
.maxBlobCount = 10000,
|
|
.currentBlobStorageBytes = 0,
|
|
.currentBlobCount = 0
|
|
};
|
|
|
|
tenant::TenantContext context(identity, quota, "tenants/tenant-123/");
|
|
|
|
// Use with blob storage
|
|
blob::MemoryStorage baseStorage;
|
|
blob::TenantAwareBlobStorage tenantStorage(&baseStorage, context);
|
|
|
|
std::vector<char> data = {'H', 'e', 'l', 'l', 'o'};
|
|
auto result = tenantStorage.upload("test.txt", data, {});
|
|
|
|
if (result.isOk()) {
|
|
std::cout << "Uploaded successfully\n";
|
|
}
|
|
```
|
|
|
|
## Security Considerations
|
|
|
|
1. **Path Traversal Prevention**: Namespace scoping prevents accessing other tenants' data
|
|
2. **Permission Checks**: All operations check permissions before execution
|
|
3. **Quota Enforcement**: Hard limits prevent resource abuse
|
|
4. **Immutable Context**: Tenant context cannot be modified during operations
|
|
5. **Audit Trail**: All operations can be logged with tenant/user info
|
|
|
|
## Performance
|
|
|
|
- **In-Memory**: O(1) for get/set operations
|
|
- **Blob Storage**: Same as underlying storage (S3/filesystem)
|
|
- **Quota Checks**: O(1) constant-time checks
|
|
- **Permission Checks**: O(1) for role-based, O(n) for specific permissions (n = permission count)
|
|
|
|
## Testing
|
|
|
|
```typescript
|
|
import { describe, it, expect } from 'vitest'
|
|
|
|
describe('Multi-Tenant System', () => {
|
|
it('isolates tenants', async () => {
|
|
const manager = new InMemoryTenantManager()
|
|
const kvStore = new InMemoryKVStore()
|
|
|
|
await manager.createTenant('tenant-1')
|
|
await manager.createTenant('tenant-2')
|
|
|
|
const ctx1 = await manager.getTenantContext('tenant-1', 'user-1')
|
|
const ctx2 = await manager.getTenantContext('tenant-2', 'user-2')
|
|
|
|
await kvStore.set('data', 'value-1', ctx1)
|
|
await kvStore.set('data', 'value-2', ctx2)
|
|
|
|
expect(await kvStore.get('data', ctx1)).toBe('value-1')
|
|
expect(await kvStore.get('data', ctx2)).toBe('value-2')
|
|
})
|
|
|
|
it('enforces quotas', async () => {
|
|
const manager = new InMemoryTenantManager()
|
|
await manager.createTenant('tenant-1', { maxRecords: 2 })
|
|
|
|
const kvStore = new InMemoryKVStore()
|
|
const ctx = await manager.getTenantContext('tenant-1', 'user-1')
|
|
|
|
await kvStore.set('key1', 'val1', ctx) // OK
|
|
await kvStore.set('key2', 'val2', ctx) // OK
|
|
await expect(kvStore.set('key3', 'val3', ctx)).rejects.toThrow('Quota exceeded')
|
|
})
|
|
|
|
it('enforces permissions', async () => {
|
|
const manager = new InMemoryTenantManager()
|
|
await manager.createTenant('tenant-1')
|
|
manager.setUserRole('tenant-1', 'viewer', 'viewer')
|
|
|
|
const kvStore = new InMemoryKVStore()
|
|
const ctx = await manager.getTenantContext('tenant-1', 'viewer')
|
|
|
|
await kvStore.set('data', 'value', ctx) // Should be OK with member role
|
|
// But viewer role should fail:
|
|
manager.setUserRole('tenant-1', 'viewer', 'viewer')
|
|
const viewerCtx = await manager.getTenantContext('tenant-1', 'viewer')
|
|
await expect(kvStore.set('data', 'new', viewerCtx)).rejects.toThrow('Permission denied')
|
|
})
|
|
})
|
|
```
|
|
|
|
## Migration Path
|
|
|
|
### Existing Applications
|
|
|
|
1. **Add tenant context**: Inject tenant/user info into all operations
|
|
2. **Wrap storage**: Use TenantAwareBlobStorage wrapper
|
|
3. **Set quotas**: Configure appropriate limits for each tenant
|
|
4. **Test isolation**: Verify data separation between tenants
|
|
|
|
### Production Deployment
|
|
|
|
1. **Gradual rollout**: Enable for new tenants first
|
|
2. **Monitor usage**: Track quota consumption
|
|
3. **Adjust limits**: Tune quotas based on actual usage
|
|
4. **Audit access**: Log all permission denials
|
|
|
|
## Future Enhancements
|
|
|
|
- [ ] Database persistence for tenant manager
|
|
- [ ] Rate limiting per tenant
|
|
- [ ] Audit logging
|
|
- [ ] Tenant analytics and reporting
|
|
- [ ] Automatic quota scaling
|
|
- [ ] Cost allocation per tenant
|
|
- [ ] Backup/restore per tenant
|
|
- [ ] Data export per tenant (GDPR compliance)
|