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:
338
packages/utils/dist/tenant.js
vendored
Normal file
338
packages/utils/dist/tenant.js
vendored
Normal file
@@ -0,0 +1,338 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.generateTenantNamespace = generateTenantNamespace;
|
||||
exports.generateTenantSubdomain = generateTenantSubdomain;
|
||||
exports.generateTenantUserId = generateTenantUserId;
|
||||
exports.generateTenantGroupId = generateTenantGroupId;
|
||||
exports.getTenantDataPath = getTenantDataPath;
|
||||
exports.getTemplateResourceLimits = getTemplateResourceLimits;
|
||||
exports.getTemplateMaxUsers = getTemplateMaxUsers;
|
||||
exports.isDomainAvailable = isDomainAvailable;
|
||||
exports.generateTenantConfig = generateTenantConfig;
|
||||
exports.generateTenantDeploymentYAML = generateTenantDeploymentYAML;
|
||||
exports.calculateTenantCosts = calculateTenantCosts;
|
||||
/**
|
||||
* Generate Kubernetes namespace name for tenant
|
||||
*/
|
||||
function generateTenantNamespace(domain) {
|
||||
return `gt-${domain}`;
|
||||
}
|
||||
/**
|
||||
* Generate tenant subdomain
|
||||
*/
|
||||
function generateTenantSubdomain(domain) {
|
||||
return domain; // For now, subdomain matches domain
|
||||
}
|
||||
/**
|
||||
* Generate OS user ID for tenant isolation
|
||||
*/
|
||||
function generateTenantUserId(tenantId) {
|
||||
const baseUserId = 10000; // Start user IDs from 10000
|
||||
return baseUserId + tenantId;
|
||||
}
|
||||
/**
|
||||
* Generate OS group ID for tenant isolation
|
||||
*/
|
||||
function generateTenantGroupId(tenantId) {
|
||||
return generateTenantUserId(tenantId); // Use same ID for group
|
||||
}
|
||||
/**
|
||||
* Get tenant data directory path
|
||||
*/
|
||||
function getTenantDataPath(domain, baseDataDir = '/data') {
|
||||
return `${baseDataDir}/${domain}`;
|
||||
}
|
||||
/**
|
||||
* Get default resource limits based on template
|
||||
*/
|
||||
function getTemplateResourceLimits(template) {
|
||||
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
|
||||
*/
|
||||
function getTemplateMaxUsers(template) {
|
||||
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)
|
||||
*/
|
||||
function isDomainAvailable(domain) {
|
||||
// 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
|
||||
*/
|
||||
function generateTenantConfig(request, masterEncryptionKey) {
|
||||
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
|
||||
*/
|
||||
function generateTenantDeploymentYAML(tenant, tenantUserId) {
|
||||
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"
|
||||
REDIS_PREFIX: "gt2:${tenant.domain}:"
|
||||
MINIO_BUCKET: "gt2-${tenant.domain}-files"
|
||||
---
|
||||
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
|
||||
*/
|
||||
function calculateTenantCosts(cpuUsage, // CPU hours
|
||||
memoryUsage, // Memory GB-hours
|
||||
storageUsage, // Storage GB-hours
|
||||
aiTokens // AI tokens used
|
||||
) {
|
||||
// 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