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:
HackWeasel
2025-12-12 17:04:45 -05:00
commit b9dfb86260
746 changed files with 232071 additions and 0 deletions

View 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
}