GT AI OS Community Edition v2.0.33
Security hardening release addressing CodeQL and Dependabot alerts: - Fix stack trace exposure in error responses - Add SSRF protection with DNS resolution checking - Implement proper URL hostname validation (replaces substring matching) - Add centralized path sanitization to prevent path traversal - Fix ReDoS vulnerability in email validation regex - Improve HTML sanitization in validation utilities - Fix capability wildcard matching in auth utilities - Update glob dependency to address CVE - Add CodeQL suppression comments for verified false positives 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
354
packages/utils/src/tenant.ts
Normal file
354
packages/utils/src/tenant.ts
Normal file
@@ -0,0 +1,354 @@
|
||||
// Tenant management utilities
|
||||
import { Tenant, TenantCreateRequest } from '@gt2/types';
|
||||
import { generateEncryptionKey, deriveTenantKey } from './crypto';
|
||||
|
||||
/**
|
||||
* Generate Kubernetes namespace name for tenant
|
||||
*/
|
||||
export function generateTenantNamespace(domain: string): string {
|
||||
return `gt-${domain}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate tenant subdomain
|
||||
*/
|
||||
export function generateTenantSubdomain(domain: string): string {
|
||||
return domain; // For now, subdomain matches domain
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate OS user ID for tenant isolation
|
||||
*/
|
||||
export function generateTenantUserId(tenantId: number): number {
|
||||
const baseUserId = 10000; // Start user IDs from 10000
|
||||
return baseUserId + tenantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate OS group ID for tenant isolation
|
||||
*/
|
||||
export function generateTenantGroupId(tenantId: number): number {
|
||||
return generateTenantUserId(tenantId); // Use same ID for group
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tenant data directory path
|
||||
*/
|
||||
export function getTenantDataPath(domain: string, baseDataDir: string = '/data'): string {
|
||||
return `${baseDataDir}/${domain}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default resource limits based on template
|
||||
*/
|
||||
export function getTemplateResourceLimits(template: string): {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
storage: string;
|
||||
} {
|
||||
switch (template) {
|
||||
case 'basic':
|
||||
return {
|
||||
cpu: '500m',
|
||||
memory: '1Gi',
|
||||
storage: '5Gi'
|
||||
};
|
||||
case 'professional':
|
||||
return {
|
||||
cpu: '1000m',
|
||||
memory: '2Gi',
|
||||
storage: '20Gi'
|
||||
};
|
||||
case 'enterprise':
|
||||
return {
|
||||
cpu: '2000m',
|
||||
memory: '4Gi',
|
||||
storage: '100Gi'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
cpu: '500m',
|
||||
memory: '1Gi',
|
||||
storage: '5Gi'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default max users based on template
|
||||
*/
|
||||
export function getTemplateMaxUsers(template: string): number {
|
||||
switch (template) {
|
||||
case 'basic':
|
||||
return 10;
|
||||
case 'professional':
|
||||
return 100;
|
||||
case 'enterprise':
|
||||
return 1000;
|
||||
default:
|
||||
return 10;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate tenant domain availability (placeholder - would check database in real implementation)
|
||||
*/
|
||||
export function isDomainAvailable(domain: string): boolean {
|
||||
// In real implementation, this would check the database
|
||||
// For now, just check format
|
||||
const reservedDomains = ['admin', 'api', 'www', 'mail', 'ftp', 'localhost', 'gt2'];
|
||||
return !reservedDomains.includes(domain.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate complete tenant configuration from create request
|
||||
*/
|
||||
export function generateTenantConfig(
|
||||
request: TenantCreateRequest,
|
||||
masterEncryptionKey: string
|
||||
): Partial<Tenant> {
|
||||
const template = request.template || 'basic';
|
||||
const resourceLimits = request.resource_limits || getTemplateResourceLimits(template);
|
||||
const maxUsers = request.max_users || getTemplateMaxUsers(template);
|
||||
|
||||
return {
|
||||
name: request.name.trim(),
|
||||
domain: request.domain.toLowerCase(),
|
||||
template,
|
||||
max_users: maxUsers,
|
||||
resource_limits: resourceLimits,
|
||||
namespace: generateTenantNamespace(request.domain),
|
||||
subdomain: generateTenantSubdomain(request.domain),
|
||||
status: 'pending'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Kubernetes deployment YAML for tenant
|
||||
*/
|
||||
export function generateTenantDeploymentYAML(tenant: Tenant, tenantUserId: number): string {
|
||||
return `
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: ${tenant.namespace}
|
||||
labels:
|
||||
gt.tenant: ${tenant.domain}
|
||||
gt.template: ${tenant.template}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: ${tenant.domain}-isolation
|
||||
namespace: ${tenant.namespace}
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes: ["Ingress", "Egress"]
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
name: gt-admin
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
name: gt-resource
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ${tenant.domain}-config
|
||||
namespace: ${tenant.namespace}
|
||||
data:
|
||||
TENANT_ID: "${tenant.id}"
|
||||
TENANT_DOMAIN: "${tenant.domain}"
|
||||
TENANT_NAME: "${tenant.name}"
|
||||
DATABASE_PATH: "/data/${tenant.domain}/app.db"
|
||||
CHROMA_COLLECTION: "gt2_${tenant.domain.replace(/-/g, '_')}_documents"
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ${tenant.domain}-app
|
||||
namespace: ${tenant.namespace}
|
||||
labels:
|
||||
app: ${tenant.domain}-app
|
||||
tenant: ${tenant.domain}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ${tenant.domain}-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ${tenant.domain}-app
|
||||
tenant: ${tenant.domain}
|
||||
spec:
|
||||
securityContext:
|
||||
runAsUser: ${tenantUserId}
|
||||
runAsGroup: ${tenantUserId}
|
||||
fsGroup: ${tenantUserId}
|
||||
containers:
|
||||
- name: frontend
|
||||
image: gt2/tenant-frontend:latest
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
name: frontend
|
||||
env:
|
||||
- name: NEXT_PUBLIC_API_URL
|
||||
value: "http://localhost:8000"
|
||||
- name: NEXT_PUBLIC_WS_URL
|
||||
value: "ws://localhost:8000"
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "128Mi"
|
||||
limits:
|
||||
cpu: "${tenant.resource_limits.cpu}"
|
||||
memory: "${tenant.resource_limits.memory}"
|
||||
volumeMounts:
|
||||
- name: tenant-data
|
||||
mountPath: /data/${tenant.domain}
|
||||
- name: backend
|
||||
image: gt2/tenant-backend:latest
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
name: backend
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: ${tenant.domain}-config
|
||||
env:
|
||||
- name: ENCRYPTION_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: ${tenant.domain}-secrets
|
||||
key: encryption-key
|
||||
resources:
|
||||
requests:
|
||||
cpu: "200m"
|
||||
memory: "256Mi"
|
||||
limits:
|
||||
cpu: "${tenant.resource_limits.cpu}"
|
||||
memory: "${tenant.resource_limits.memory}"
|
||||
volumeMounts:
|
||||
- name: tenant-data
|
||||
mountPath: /data/${tenant.domain}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: 8000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: tenant-data
|
||||
persistentVolumeClaim:
|
||||
claimName: ${tenant.domain}-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: ${tenant.domain}-data
|
||||
namespace: ${tenant.namespace}
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: ${tenant.resource_limits.storage}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ${tenant.domain}-secrets
|
||||
namespace: ${tenant.namespace}
|
||||
type: Opaque
|
||||
data:
|
||||
encryption-key: ${Buffer.from(tenant.encryption_key || '').toString('base64')}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ${tenant.domain}-service
|
||||
namespace: ${tenant.namespace}
|
||||
spec:
|
||||
selector:
|
||||
app: ${tenant.domain}-app
|
||||
ports:
|
||||
- name: frontend
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
- name: backend
|
||||
port: 8000
|
||||
targetPort: 8000
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: ${tenant.domain}-ingress
|
||||
namespace: ${tenant.namespace}
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
spec:
|
||||
rules:
|
||||
- host: ${tenant.subdomain}.gt2.local
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ${tenant.domain}-service
|
||||
port:
|
||||
number: 8000
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: ${tenant.domain}-service
|
||||
port:
|
||||
number: 3000
|
||||
`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate tenant usage costs
|
||||
*/
|
||||
export function calculateTenantCosts(
|
||||
cpuUsage: number, // CPU hours
|
||||
memoryUsage: number, // Memory GB-hours
|
||||
storageUsage: number, // Storage GB-hours
|
||||
aiTokens: number // AI tokens used
|
||||
): {
|
||||
cpu_cost_cents: number;
|
||||
memory_cost_cents: number;
|
||||
storage_cost_cents: number;
|
||||
ai_cost_cents: number;
|
||||
total_cost_cents: number;
|
||||
} {
|
||||
// Pricing (example rates)
|
||||
const CPU_COST_PER_HOUR = 5; // 5 cents per CPU hour
|
||||
const MEMORY_COST_PER_GB_HOUR = 1; // 1 cent per GB-hour
|
||||
const STORAGE_COST_PER_GB_HOUR = 0.1; // 0.1 cents per GB-hour
|
||||
const AI_COST_PER_1K_TOKENS = 0.5; // 0.5 cents per 1K tokens
|
||||
|
||||
const cpu_cost_cents = Math.round(cpuUsage * CPU_COST_PER_HOUR);
|
||||
const memory_cost_cents = Math.round(memoryUsage * MEMORY_COST_PER_GB_HOUR);
|
||||
const storage_cost_cents = Math.round(storageUsage * STORAGE_COST_PER_GB_HOUR);
|
||||
const ai_cost_cents = Math.round((aiTokens / 1000) * AI_COST_PER_1K_TOKENS);
|
||||
|
||||
return {
|
||||
cpu_cost_cents,
|
||||
memory_cost_cents,
|
||||
storage_cost_cents,
|
||||
ai_cost_cents,
|
||||
total_cost_cents: cpu_cost_cents + memory_cost_cents + storage_cost_cents + ai_cost_cents
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user