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:
163
apps/control-panel-backend/app/models/tenant.py
Normal file
163
apps/control-panel-backend/app/models/tenant.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
Tenant database model
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, ForeignKey, UniqueConstraint, JSON, Numeric
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class Tenant(Base):
|
||||
"""Tenant model for multi-tenancy"""
|
||||
|
||||
__tablename__ = "tenants"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
uuid = Column(String(36), default=lambda: str(uuid.uuid4()), unique=True, nullable=False)
|
||||
name = Column(String(100), nullable=False)
|
||||
domain = Column(String(50), unique=True, nullable=False, index=True)
|
||||
template = Column(String(20), nullable=False, default="basic")
|
||||
status = Column(
|
||||
String(20),
|
||||
nullable=False,
|
||||
default="pending",
|
||||
index=True
|
||||
) # pending, deploying, active, suspended, terminated
|
||||
max_users = Column(Integer, nullable=False, default=100)
|
||||
resource_limits = Column(
|
||||
JSON,
|
||||
nullable=False,
|
||||
default=lambda: {"cpu": "1000m", "memory": "2Gi", "storage": "10Gi"}
|
||||
)
|
||||
namespace = Column(String(100), unique=True, nullable=False)
|
||||
subdomain = Column(String(50), unique=True, nullable=False)
|
||||
database_path = Column(String(255), nullable=True)
|
||||
encryption_key = Column(Text, nullable=True)
|
||||
|
||||
# Frontend URL (for password reset emails, etc.)
|
||||
# If not set, defaults to http://localhost:3002
|
||||
frontend_url = Column(String(255), nullable=True)
|
||||
|
||||
# API Keys (encrypted)
|
||||
api_keys = Column(JSON, default=dict) # {"groq": {"key": "encrypted", "enabled": true}, ...}
|
||||
api_key_encryption_version = Column(String(20), default="v1")
|
||||
|
||||
# Feature toggles
|
||||
optics_enabled = Column(Boolean, default=False) # Enable Optics cost tracking tab
|
||||
|
||||
# Budget fields (Issue #234)
|
||||
monthly_budget_cents = Column(Integer, nullable=True) # NULL = unlimited
|
||||
budget_warning_threshold = Column(Integer, default=80) # Percentage
|
||||
budget_critical_threshold = Column(Integer, default=90) # Percentage
|
||||
budget_enforcement_enabled = Column(Boolean, default=True)
|
||||
|
||||
# Per-tenant storage pricing overrides (Issue #218)
|
||||
# Hot tier: NULL = use system default ($0.15/GiB/month)
|
||||
storage_price_dataset_hot = Column(Numeric(10, 4), nullable=True)
|
||||
storage_price_conversation_hot = Column(Numeric(10, 4), nullable=True)
|
||||
|
||||
# Cold tier: Allocation-based model
|
||||
# Monthly cost = allocated_tibs × price_per_tib
|
||||
cold_storage_allocated_tibs = Column(Numeric(10, 4), nullable=True) # NULL = no cold storage
|
||||
cold_storage_price_per_tib = Column(Numeric(10, 2), nullable=True, default=10.00) # Default $10/TiB/month
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Relationships
|
||||
# users relationship replaced with user_assignments for multi-tenant support
|
||||
user_assignments = relationship("UserTenantAssignment", back_populates="tenant", cascade="all, delete-orphan")
|
||||
tenant_resources = relationship("TenantResource", back_populates="tenant", cascade="all, delete-orphan")
|
||||
usage_records = relationship("UsageRecord", back_populates="tenant", cascade="all, delete-orphan")
|
||||
audit_logs = relationship("AuditLog", back_populates="tenant", cascade="all, delete-orphan")
|
||||
|
||||
# Resource management relationships
|
||||
resource_quotas = relationship("ResourceQuota", back_populates="tenant", cascade="all, delete-orphan")
|
||||
resource_usage_records = relationship("ResourceUsage", back_populates="tenant", cascade="all, delete-orphan")
|
||||
resource_alerts = relationship("ResourceAlert", back_populates="tenant", cascade="all, delete-orphan")
|
||||
|
||||
# Model access relationships
|
||||
model_configs = relationship("TenantModelConfig", back_populates="tenant", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Tenant(id={self.id}, domain='{self.domain}', status='{self.status}')>"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert tenant to dictionary"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"uuid": str(self.uuid),
|
||||
"name": self.name,
|
||||
"domain": self.domain,
|
||||
"template": self.template,
|
||||
"status": self.status,
|
||||
"max_users": self.max_users,
|
||||
"resource_limits": self.resource_limits,
|
||||
"namespace": self.namespace,
|
||||
"subdomain": self.subdomain,
|
||||
"frontend_url": self.frontend_url,
|
||||
"api_keys_configured": {k: v.get('enabled', False) for k, v in (self.api_keys or {}).items()},
|
||||
"optics_enabled": self.optics_enabled or False,
|
||||
"monthly_budget_cents": self.monthly_budget_cents,
|
||||
"budget_warning_threshold": self.budget_warning_threshold or 80,
|
||||
"budget_critical_threshold": self.budget_critical_threshold or 90,
|
||||
"budget_enforcement_enabled": self.budget_enforcement_enabled or False,
|
||||
"storage_price_dataset_hot": float(self.storage_price_dataset_hot) if self.storage_price_dataset_hot else None,
|
||||
"storage_price_conversation_hot": float(self.storage_price_conversation_hot) if self.storage_price_conversation_hot else None,
|
||||
"cold_storage_allocated_tibs": float(self.cold_storage_allocated_tibs) if self.cold_storage_allocated_tibs else None,
|
||||
"cold_storage_price_per_tib": float(self.cold_storage_price_per_tib) if self.cold_storage_price_per_tib else 10.00,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
"""Check if tenant is active"""
|
||||
return self.status == "active" and self.deleted_at is None
|
||||
|
||||
|
||||
class TenantResource(Base):
|
||||
"""Tenant resource assignments"""
|
||||
|
||||
__tablename__ = "tenant_resources"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
tenant_id = Column(Integer, ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False)
|
||||
resource_id = Column(Integer, ForeignKey("ai_resources.id", ondelete="CASCADE"), nullable=False)
|
||||
usage_limits = Column(
|
||||
JSON,
|
||||
nullable=False,
|
||||
default=lambda: {"max_requests_per_hour": 1000, "max_tokens_per_request": 4000}
|
||||
)
|
||||
is_enabled = Column(Boolean, nullable=False, default=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
|
||||
# Relationships
|
||||
tenant = relationship("Tenant", back_populates="tenant_resources")
|
||||
ai_resource = relationship("AIResource", back_populates="tenant_resources")
|
||||
|
||||
# Unique constraint
|
||||
__table_args__ = (
|
||||
UniqueConstraint('tenant_id', 'resource_id', name='unique_tenant_resource'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<TenantResource(tenant_id={self.tenant_id}, resource_id={self.resource_id})>"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert tenant resource to dictionary"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"tenant_id": self.tenant_id,
|
||||
"resource_id": self.resource_id,
|
||||
"usage_limits": self.usage_limits,
|
||||
"is_enabled": self.is_enabled,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
Reference in New Issue
Block a user