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,410 @@
"""
Team Access Control Service for GT 2.0 Tenant Backend
Implements team-based access control with file-based simplicity.
Follows GT 2.0's principle of "Zero Complexity Addition"
- Simple role-based permissions stored in files
- Fast access checks using SQLite indexes
- Perfect tenant isolation maintained
"""
from typing import Dict, Any, List, Optional, Tuple
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_, or_
import logging
from app.models.team import Team, TeamRole, OrganizationSettings
from app.models.agent import Agent
from app.models.document import RAGDataset
logger = logging.getLogger(__name__)
class TeamAccessService:
"""Elegant team-based access control following GT 2.0 philosophy"""
def __init__(self, db: AsyncSession):
self.db = db
self._role_cache = {} # Cache role permissions in memory
async def check_team_access(
self,
user_email: str,
resource: Any,
action: str,
user_teams: Optional[List[int]] = None
) -> bool:
"""Check if user has access to perform action on resource
GT 2.0 Design: Simple, fast access checks without complex hierarchies
"""
try:
# Step 1: Check resource ownership (fastest check)
if hasattr(resource, 'created_by') and resource.created_by == user_email:
return True # Owners always have full access
# Step 2: Check visibility-based access
if hasattr(resource, 'visibility'):
# Organization-wide resources
if resource.visibility == "organization":
return self._check_organization_action(action)
# Team resources
if resource.visibility == "team" and resource.tenant_id:
if not user_teams:
user_teams = await self.get_user_teams(user_email)
if resource.tenant_id in user_teams:
return await self._check_team_action(
user_email,
resource.tenant_id,
action
)
# Explicitly shared resources
if hasattr(resource, 'shared_with') and resource.shared_with:
if user_email in resource.shared_with:
return self._check_shared_action(action)
# Step 3: Default deny for private resources not owned by user
return False
except Exception as e:
logger.error(f"Error checking team access: {e}")
return False # Fail closed on errors
async def get_user_teams(self, user_email: str) -> List[int]:
"""Get all teams the user belongs to
GT 2.0: Simple file-based membership check
"""
try:
# Query all active teams
result = await self.db.execute(
select(Team).where(Team.is_active == True)
)
teams = result.scalars().all()
user_team_ids = []
for team in teams:
if team.is_member(user_email):
user_team_ids.append(team.id)
return user_team_ids
except Exception as e:
logger.error(f"Error getting user teams: {e}")
return []
async def get_user_role_in_team(self, user_email: str, team_id: int) -> Optional[str]:
"""Get user's role in a specific team"""
try:
result = await self.db.execute(
select(Team).where(Team.id == team_id)
)
team = result.scalar_one_or_none()
if team:
return team.get_member_role(user_email)
return None
except Exception as e:
logger.error(f"Error getting user role: {e}")
return None
async def get_team_resources(
self,
team_id: int,
resource_type: str,
user_email: str
) -> List[Any]:
"""Get all resources accessible to a team
GT 2.0: Simple visibility-based filtering
"""
try:
if resource_type == "agent":
# Get team and organization agents
result = await self.db.execute(
select(Agent).where(
and_(
Agent.is_active == True,
or_(
and_(
Agent.visibility == "team",
Agent.tenant_id == team_id
),
Agent.visibility == "organization"
)
)
)
)
return result.scalars().all()
elif resource_type == "dataset":
# Get team and organization datasets
result = await self.db.execute(
select(RAGDataset).where(
and_(
RAGDataset.status == "active",
or_(
and_(
RAGDataset.visibility == "team",
RAGDataset.tenant_id == team_id
),
RAGDataset.visibility == "organization"
)
)
)
)
return result.scalars().all()
return []
except Exception as e:
logger.error(f"Error getting team resources: {e}")
return []
async def share_with_team(
self,
resource: Any,
team_id: int,
sharer_email: str
) -> bool:
"""Share a resource with a team
GT 2.0: Simple visibility update, no complex permissions
"""
try:
# Verify sharer owns the resource or has sharing permission
if not self._can_share_resource(resource, sharer_email):
return False
# Update resource visibility
resource.visibility = "team"
resource.tenant_id = team_id
await self.db.commit()
return True
except Exception as e:
logger.error(f"Error sharing with team: {e}")
await self.db.rollback()
return False
async def share_with_users(
self,
resource: Any,
user_emails: List[str],
sharer_email: str
) -> bool:
"""Share a resource with specific users
GT 2.0: Simple list-based sharing
"""
try:
# Verify sharer owns the resource
if not self._can_share_resource(resource, sharer_email):
return False
# Update shared_with list
current_shared = resource.shared_with or []
new_shared = list(set(current_shared + user_emails))
resource.shared_with = new_shared
await self.db.commit()
return True
except Exception as e:
logger.error(f"Error sharing with users: {e}")
await self.db.rollback()
return False
async def create_team(
self,
name: str,
description: str,
team_type: str,
creator_email: str
) -> Optional[Team]:
"""Create a new team
GT 2.0: File-based team with simple SQLite reference
"""
try:
# Check if user can create teams
org_settings = await self._get_organization_settings()
if not org_settings.allow_team_creation:
logger.warning(f"Team creation disabled for organization")
return None
# Check user's team limit
user_teams = await self.get_user_teams(creator_email)
if len(user_teams) >= org_settings.max_teams_per_user:
logger.warning(f"User {creator_email} reached team limit")
return None
# Create team
team = Team(
name=name,
description=description,
team_type=team_type,
created_by=creator_email
)
# Initialize with placeholder paths
team.config_file_path = "placeholder"
team.members_file_path = "placeholder"
# Save to get ID
self.db.add(team)
await self.db.flush()
# Initialize proper file paths
team.initialize_file_paths()
# Add creator as owner
team.add_member(creator_email, "owner", {"joined_as": "creator"})
# Save initial config
config = {
"name": name,
"description": description,
"team_type": team_type,
"created_by": creator_email,
"settings": {}
}
team.save_config_to_file(config)
await self.db.commit()
await self.db.refresh(team)
logger.info(f"Created team {team.id} by {creator_email}")
return team
except Exception as e:
logger.error(f"Error creating team: {e}")
await self.db.rollback()
return None
# Private helper methods
def _check_organization_action(self, action: str) -> bool:
"""Check if action is allowed for organization resources"""
# Organization resources are viewable by all
if action in ["view", "use", "read"]:
return True
# Only owners can modify
return False
async def _check_team_action(
self,
user_email: str,
team_id: int,
action: str
) -> bool:
"""Check if user can perform action on team resource"""
role = await self.get_user_role_in_team(user_email, team_id)
if not role:
return False
# Get role permissions
permissions = await self._get_role_permissions(role)
# Map action to permission
action_permission_map = {
"view": "can_view_resources",
"read": "can_view_resources",
"use": "can_view_resources",
"create": "can_create_resources",
"edit": "can_edit_team_resources",
"update": "can_edit_team_resources",
"delete": "can_delete_team_resources",
"manage_members": "can_manage_members",
"manage_team": "can_manage_team",
}
permission_needed = action_permission_map.get(action, None)
if permission_needed:
return permissions.get(permission_needed, False)
return False
def _check_shared_action(self, action: str) -> bool:
"""Check if action is allowed for shared resources"""
# Shared resources can be viewed and used
if action in ["view", "use", "read"]:
return True
return False
def _can_share_resource(self, resource: Any, user_email: str) -> bool:
"""Check if user can share a resource"""
# Owners can always share
if hasattr(resource, 'created_by') and resource.created_by == user_email:
return True
# Team leads can share team resources
# (Would need to check team role here in full implementation)
return False
async def _get_role_permissions(self, role_name: str) -> Dict[str, bool]:
"""Get permissions for a role (with caching)"""
if role_name in self._role_cache:
return self._role_cache[role_name]
result = await self.db.execute(
select(TeamRole).where(TeamRole.name == role_name)
)
role = result.scalar_one_or_none()
if role:
permissions = {
"can_view_resources": role.can_view_resources,
"can_create_resources": role.can_create_resources,
"can_edit_team_resources": role.can_edit_team_resources,
"can_delete_team_resources": role.can_delete_team_resources,
"can_manage_members": role.can_manage_members,
"can_manage_team": role.can_manage_team,
}
self._role_cache[role_name] = permissions
return permissions
# Default to viewer permissions
return {
"can_view_resources": True,
"can_create_resources": False,
"can_edit_team_resources": False,
"can_delete_team_resources": False,
"can_manage_members": False,
"can_manage_team": False,
}
async def _get_organization_settings(self) -> OrganizationSettings:
"""Get organization settings (create default if not exists)"""
result = await self.db.execute(
select(OrganizationSettings).limit(1)
)
settings = result.scalar_one_or_none()
if not settings:
# Create default settings
settings = OrganizationSettings(
organization_name="Default Organization",
organization_domain="example.com"
)
settings.config_file_path = "placeholder"
self.db.add(settings)
await self.db.flush()
settings.initialize_file_paths()
settings.save_config_to_file({
"initialized": True,
"default_config": True
})
await self.db.commit()
await self.db.refresh(settings)
return settings