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,233 @@
"""
GT 2.0 User Service - User Preferences Management
Manages user preferences including favorite agents using PostgreSQL + PGVector backend.
Perfect tenant isolation - each tenant has separate user data.
"""
import json
import logging
from typing import List, Optional, Dict, Any
from app.core.config import get_settings
from app.core.postgresql_client import get_postgresql_client
logger = logging.getLogger(__name__)
class UserService:
"""GT 2.0 PostgreSQL User Service with Perfect Tenant Isolation"""
def __init__(self, tenant_domain: str, user_id: str, user_email: str = None):
"""Initialize with tenant and user isolation using PostgreSQL storage"""
self.tenant_domain = tenant_domain
self.user_id = user_id
self.user_email = user_email or user_id
self.settings = get_settings()
logger.info(f"User service initialized for {tenant_domain}/{user_id} (email: {self.user_email})")
async def _get_user_id(self, pg_client) -> Optional[str]:
"""Get user ID from email or user_id with fallback"""
user_lookup_query = """
SELECT id FROM users
WHERE (email = $1 OR id::text = $1 OR username = $1)
AND tenant_id = (SELECT id FROM tenants WHERE domain = $2 LIMIT 1)
LIMIT 1
"""
user_id = await pg_client.fetch_scalar(user_lookup_query, self.user_email, self.tenant_domain)
if not user_id:
# If not found by email, try by user_id
user_id = await pg_client.fetch_scalar(user_lookup_query, self.user_id, self.tenant_domain)
return user_id
async def get_user_preferences(self) -> Dict[str, Any]:
"""Get user preferences from PostgreSQL"""
try:
pg_client = await get_postgresql_client()
user_id = await self._get_user_id(pg_client)
if not user_id:
logger.warning(f"User not found: {self.user_email} (or {self.user_id}) in tenant {self.tenant_domain}")
return {}
query = """
SELECT preferences
FROM users
WHERE id = $1
AND tenant_id = (SELECT id FROM tenants WHERE domain = $2 LIMIT 1)
"""
result = await pg_client.fetch_one(query, user_id, self.tenant_domain)
if result and result["preferences"]:
prefs = result["preferences"]
# Handle both dict and JSON string
if isinstance(prefs, str):
return json.loads(prefs)
return prefs
return {}
except Exception as e:
logger.error(f"Error getting user preferences: {e}")
return {}
async def update_user_preferences(self, preferences: Dict[str, Any]) -> bool:
"""Update user preferences in PostgreSQL (merges with existing)"""
try:
pg_client = await get_postgresql_client()
user_id = await self._get_user_id(pg_client)
if not user_id:
logger.warning(f"User not found: {self.user_email} (or {self.user_id}) in tenant {self.tenant_domain}")
return False
# Merge with existing preferences using PostgreSQL JSONB || operator
query = """
UPDATE users
SET preferences = COALESCE(preferences, '{}'::jsonb) || $1::jsonb,
updated_at = NOW()
WHERE id = $2
AND tenant_id = (SELECT id FROM tenants WHERE domain = $3 LIMIT 1)
RETURNING id
"""
updated_id = await pg_client.fetch_scalar(
query,
json.dumps(preferences),
user_id,
self.tenant_domain
)
if updated_id:
logger.info(f"Updated preferences for user {user_id}")
return True
return False
except Exception as e:
logger.error(f"Error updating user preferences: {e}")
return False
async def get_favorite_agent_ids(self) -> List[str]:
"""Get user's favorited agent IDs"""
try:
preferences = await self.get_user_preferences()
favorite_ids = preferences.get("favorite_agent_ids", [])
# Ensure it's a list
if not isinstance(favorite_ids, list):
return []
logger.info(f"Retrieved {len(favorite_ids)} favorite agent IDs for user {self.user_id}")
return favorite_ids
except Exception as e:
logger.error(f"Error getting favorite agent IDs: {e}")
return []
async def update_favorite_agent_ids(self, agent_ids: List[str]) -> bool:
"""Update user's favorited agent IDs"""
try:
# Validate agent_ids is a list
if not isinstance(agent_ids, list):
logger.error(f"Invalid agent_ids type: {type(agent_ids)}")
return False
# Update preferences with new favorite_agent_ids
success = await self.update_user_preferences({
"favorite_agent_ids": agent_ids
})
if success:
logger.info(f"Updated {len(agent_ids)} favorite agent IDs for user {self.user_id}")
return success
except Exception as e:
logger.error(f"Error updating favorite agent IDs: {e}")
return False
async def add_favorite_agent(self, agent_id: str) -> bool:
"""Add a single agent to favorites"""
try:
current_favorites = await self.get_favorite_agent_ids()
if agent_id not in current_favorites:
current_favorites.append(agent_id)
return await self.update_favorite_agent_ids(current_favorites)
# Already in favorites
return True
except Exception as e:
logger.error(f"Error adding favorite agent: {e}")
return False
async def remove_favorite_agent(self, agent_id: str) -> bool:
"""Remove a single agent from favorites"""
try:
current_favorites = await self.get_favorite_agent_ids()
if agent_id in current_favorites:
current_favorites.remove(agent_id)
return await self.update_favorite_agent_ids(current_favorites)
# Not in favorites
return True
except Exception as e:
logger.error(f"Error removing favorite agent: {e}")
return False
async def get_custom_categories(self) -> List[Dict[str, Any]]:
"""Get user's custom agent categories"""
try:
preferences = await self.get_user_preferences()
custom_categories = preferences.get("custom_categories", [])
# Ensure it's a list
if not isinstance(custom_categories, list):
return []
logger.info(f"Retrieved {len(custom_categories)} custom categories for user {self.user_id}")
return custom_categories
except Exception as e:
logger.error(f"Error getting custom categories: {e}")
return []
async def update_custom_categories(self, categories: List[Dict[str, Any]]) -> bool:
"""Update user's custom agent categories (replaces entire list)"""
try:
# Validate categories is a list
if not isinstance(categories, list):
logger.error(f"Invalid categories type: {type(categories)}")
return False
# Convert Pydantic models to dicts if needed
category_dicts = []
for cat in categories:
if hasattr(cat, 'dict'):
category_dicts.append(cat.dict())
elif isinstance(cat, dict):
category_dicts.append(cat)
else:
logger.error(f"Invalid category type: {type(cat)}")
return False
# Update preferences with new custom_categories
success = await self.update_user_preferences({
"custom_categories": category_dicts
})
if success:
logger.info(f"Updated {len(category_dicts)} custom categories for user {self.user_id}")
return success
except Exception as e:
logger.error(f"Error updating custom categories: {e}")
return False