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>
339 lines
9.0 KiB
JavaScript
339 lines
9.0 KiB
JavaScript
"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
|
|
};
|
|
}
|