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:
263
apps/tenant-backend/app/models/collaboration_team.py
Normal file
263
apps/tenant-backend/app/models/collaboration_team.py
Normal file
@@ -0,0 +1,263 @@
|
||||
"""
|
||||
Collaboration Team Models for GT 2.0 Tenant Backend
|
||||
|
||||
Pydantic models for user collaboration teams (team sharing system).
|
||||
This is separate from the tenant isolation 'tenants' table (formerly 'teams').
|
||||
|
||||
Database Schema:
|
||||
- teams: User collaboration groups within a tenant
|
||||
- team_memberships: Team members with two-tier permissions
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict, Any
|
||||
from pydantic import BaseModel, Field, ConfigDict, field_validator
|
||||
|
||||
|
||||
class TeamBase(BaseModel):
|
||||
"""Base team model with common fields"""
|
||||
name: str = Field(..., min_length=1, max_length=255, description="Team name")
|
||||
description: Optional[str] = Field(None, description="Team description")
|
||||
|
||||
|
||||
class TeamCreate(TeamBase):
|
||||
"""Model for creating a new team"""
|
||||
pass
|
||||
|
||||
|
||||
class TeamUpdate(BaseModel):
|
||||
"""Model for updating a team"""
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class TeamMember(BaseModel):
|
||||
"""Team member with permissions"""
|
||||
id: str = Field(..., description="Membership UUID")
|
||||
team_id: str = Field(..., description="Team UUID")
|
||||
user_id: str = Field(..., description="User UUID")
|
||||
user_email: str = Field(..., description="User email")
|
||||
user_name: str = Field(..., description="User display name")
|
||||
team_permission: str = Field(..., description="Team-level permission: 'read', 'share', or 'manager'")
|
||||
resource_permissions: Dict[str, str] = Field(default_factory=dict, description="Resource-level permissions JSONB")
|
||||
is_owner: bool = Field(default=False, description="Whether this member is the team owner")
|
||||
is_observable: bool = Field(default=False, description="Member consents to activity observation")
|
||||
observable_consent_status: str = Field(default="none", description="Consent status: 'none', 'pending', 'approved', 'revoked'")
|
||||
observable_consent_at: Optional[str] = Field(None, description="When Observable status was approved")
|
||||
status: str = Field(default="accepted", description="Membership status: 'pending', 'accepted', or 'declined'")
|
||||
invited_at: Optional[str] = None
|
||||
responded_at: Optional[str] = None
|
||||
joined_at: Optional[str] = None
|
||||
created_at: Optional[str] = None
|
||||
updated_at: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class Team(TeamBase):
|
||||
"""Complete team model with metadata"""
|
||||
id: str = Field(..., description="Team UUID")
|
||||
tenant_id: str = Field(..., description="Tenant UUID")
|
||||
owner_id: str = Field(..., description="Owner user UUID")
|
||||
owner_name: Optional[str] = Field(None, description="Owner display name")
|
||||
owner_email: Optional[str] = Field(None, description="Owner email")
|
||||
is_owner: bool = Field(..., description="Whether current user is the owner")
|
||||
can_manage: bool = Field(..., description="Whether current user can manage the team")
|
||||
user_permission: Optional[str] = Field(None, description="Current user's team permission: 'read' or 'share' (None if owner)")
|
||||
member_count: int = Field(0, description="Number of team members")
|
||||
shared_resource_count: int = Field(0, description="Number of shared resources (agents and datasets)")
|
||||
created_at: Optional[str] = None
|
||||
updated_at: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class TeamWithMembers(Team):
|
||||
"""Team with full member list"""
|
||||
members: List[TeamMember] = Field(default_factory=list, description="List of team members")
|
||||
|
||||
|
||||
class TeamListResponse(BaseModel):
|
||||
"""Response model for listing teams"""
|
||||
data: List[Team]
|
||||
total: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class TeamResponse(BaseModel):
|
||||
"""Response model for single team operation"""
|
||||
data: Team
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class TeamWithMembersResponse(BaseModel):
|
||||
"""Response model for team with members"""
|
||||
data: TeamWithMembers
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# Team Membership Models
|
||||
|
||||
class AddMemberRequest(BaseModel):
|
||||
"""Request model for adding a member to a team"""
|
||||
user_email: str = Field(..., description="Email of user to add")
|
||||
team_permission: str = Field("read", description="Team permission: 'read', 'share', or 'manager'")
|
||||
|
||||
|
||||
class UpdateMemberPermissionRequest(BaseModel):
|
||||
"""Request model for updating member permission"""
|
||||
team_permission: str = Field(..., description="New permission: 'read', 'share', or 'manager'")
|
||||
|
||||
@field_validator('team_permission')
|
||||
@classmethod
|
||||
def validate_permission(cls, v: str) -> str:
|
||||
if v not in ["read", "share", "manager"]:
|
||||
raise ValueError(f"Invalid permission: {v}. Must be 'read', 'share', or 'manager'")
|
||||
return v
|
||||
|
||||
|
||||
class MemberListResponse(BaseModel):
|
||||
"""Response model for listing team members"""
|
||||
data: List[TeamMember]
|
||||
total: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class MemberResponse(BaseModel):
|
||||
"""Response model for single member operation"""
|
||||
data: TeamMember
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# Team Invitation Models
|
||||
|
||||
class TeamInvitation(BaseModel):
|
||||
"""Pending team invitation"""
|
||||
id: str = Field(..., description="Invitation (membership) UUID")
|
||||
team_id: str = Field(..., description="Team UUID")
|
||||
team_name: str = Field(..., description="Team name")
|
||||
team_description: Optional[str] = Field(None, description="Team description")
|
||||
owner_name: str = Field(..., description="Team owner display name")
|
||||
owner_email: str = Field(..., description="Team owner email")
|
||||
team_permission: str = Field(..., description="Invited permission: 'read', 'share', or 'manager'")
|
||||
observable_requested: bool = Field(default=False, description="Whether Observable access was requested on invite")
|
||||
invited_at: str = Field(..., description="Invitation timestamp")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class InvitationActionRequest(BaseModel):
|
||||
"""Request to accept or decline invitation"""
|
||||
action: str = Field(..., description="Action: 'accept' or 'decline'")
|
||||
|
||||
|
||||
class InvitationListResponse(BaseModel):
|
||||
"""Response model for listing invitations"""
|
||||
data: List[TeamInvitation]
|
||||
total: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# Resource Sharing Models
|
||||
|
||||
class ShareResourceRequest(BaseModel):
|
||||
"""Request model for sharing a resource to team"""
|
||||
resource_type: str = Field(..., description="Resource type: 'agent' or 'dataset'")
|
||||
resource_id: str = Field(..., description="Resource UUID")
|
||||
user_permissions: Dict[str, str] = Field(
|
||||
...,
|
||||
description="User permissions: {user_id: 'read'|'edit'}"
|
||||
)
|
||||
|
||||
|
||||
class SharedResource(BaseModel):
|
||||
"""Model for a shared resource"""
|
||||
resource_type: str = Field(..., description="Resource type: 'agent' or 'dataset'")
|
||||
resource_id: str = Field(..., description="Resource UUID")
|
||||
resource_name: str = Field(..., description="Resource name")
|
||||
resource_owner: str = Field(..., description="Resource owner name or email")
|
||||
user_permissions: Dict[str, str] = Field(..., description="User permissions map")
|
||||
|
||||
|
||||
class SharedResourcesResponse(BaseModel):
|
||||
"""Response model for listing shared resources"""
|
||||
data: List[SharedResource]
|
||||
total: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# Observable Request Models
|
||||
|
||||
class ObservableRequest(BaseModel):
|
||||
"""Observable access request for a team member"""
|
||||
team_id: str = Field(..., description="Team UUID")
|
||||
team_name: str = Field(..., description="Team name")
|
||||
requested_by_name: str = Field(..., description="Name of manager/owner who requested")
|
||||
requested_by_email: str = Field(..., description="Email of manager/owner who requested")
|
||||
requested_at: str = Field(..., description="When request was made")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ObservableRequestListResponse(BaseModel):
|
||||
"""Response model for listing Observable requests"""
|
||||
data: List[ObservableRequest]
|
||||
total: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# Team Activity Models
|
||||
|
||||
class TeamActivityMetrics(BaseModel):
|
||||
"""Team activity metrics for Observable members"""
|
||||
team_id: str
|
||||
team_name: str
|
||||
date_range_days: int
|
||||
observable_member_count: int
|
||||
total_member_count: int
|
||||
team_totals: Dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Aggregated metrics: conversations, messages, tokens"
|
||||
)
|
||||
member_breakdown: List[Dict[str, Any]] = Field(
|
||||
default_factory=list,
|
||||
description="Per-member activity stats"
|
||||
)
|
||||
time_series: List[Dict[str, Any]] = Field(
|
||||
default_factory=list,
|
||||
description="Activity over time"
|
||||
)
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class TeamActivityResponse(BaseModel):
|
||||
"""Response model for team activity"""
|
||||
data: TeamActivityMetrics
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# Error Response Models
|
||||
|
||||
class ErrorDetail(BaseModel):
|
||||
"""Error detail model"""
|
||||
message: str
|
||||
field: Optional[str] = None
|
||||
code: Optional[str] = None
|
||||
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
"""Error response model"""
|
||||
error: str
|
||||
details: Optional[List[ErrorDetail]] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
Reference in New Issue
Block a user