Files
gt-ai-os-community/apps/control-panel-backend/app/models/session.py
HackWeasel b9dfb86260 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>
2025-12-12 17:04:45 -05:00

91 lines
3.8 KiB
Python

"""
Session database model for server-side session tracking.
OWASP/NIST Compliant Session Management (Issue #264):
- Server-side session state is authoritative
- Tracks idle timeout (30 min) and absolute timeout (8 hours)
- Session token hash stored (never plaintext)
"""
from datetime import datetime
from typing import Optional, Dict, Any
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import uuid
from app.core.database import Base
class Session(Base):
"""Server-side session model for OWASP/NIST compliant session management"""
__tablename__ = "sessions"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
session_token_hash = Column(String(64), unique=True, nullable=False, index=True) # SHA-256 hash
# Session timing (NIST SP 800-63B compliant)
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
last_activity_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
absolute_expires_at = Column(DateTime(timezone=True), nullable=False)
# Session metadata for security auditing
ip_address = Column(String(45), nullable=True) # IPv6 compatible
user_agent = Column(Text, nullable=True)
tenant_id = Column(Integer, ForeignKey("tenants.id"), nullable=True, index=True)
# Session state
is_active = Column(Boolean, default=True, nullable=False)
revoked_at = Column(DateTime(timezone=True), nullable=True)
revoke_reason = Column(String(50), nullable=True) # 'logout', 'idle_timeout', 'absolute_timeout', 'admin_revoke', 'password_change', 'cleanup_stale'
ended_at = Column(DateTime(timezone=True), nullable=True) # When session ended (any reason: logout, timeout, etc.)
app_type = Column(String(20), default='control_panel', nullable=False) # 'control_panel' or 'tenant_app'
# Relationships
user = relationship("User", back_populates="sessions")
tenant = relationship("Tenant", backref="sessions")
def __repr__(self):
return f"<Session(id={self.id}, user_id={self.user_id}, is_active={self.is_active})>"
def to_dict(self) -> Dict[str, Any]:
"""Convert session to dictionary (excluding sensitive data)"""
return {
"id": str(self.id),
"user_id": self.user_id,
"tenant_id": self.tenant_id,
"created_at": self.created_at.isoformat() if self.created_at else None,
"last_activity_at": self.last_activity_at.isoformat() if self.last_activity_at else None,
"absolute_expires_at": self.absolute_expires_at.isoformat() if self.absolute_expires_at else None,
"ip_address": self.ip_address,
"is_active": self.is_active,
"revoked_at": self.revoked_at.isoformat() if self.revoked_at else None,
"revoke_reason": self.revoke_reason,
"ended_at": self.ended_at.isoformat() if self.ended_at else None,
"app_type": self.app_type,
}
@property
def is_expired(self) -> bool:
"""Check if session is expired (either idle or absolute)"""
if not self.is_active:
return True
now = datetime.now(self.absolute_expires_at.tzinfo) if self.absolute_expires_at.tzinfo else datetime.utcnow()
# Check absolute timeout
if now >= self.absolute_expires_at:
return True
# Check idle timeout (30 minutes)
from datetime import timedelta
idle_timeout = timedelta(minutes=30)
idle_expires_at = self.last_activity_at + idle_timeout
if now >= idle_expires_at:
return True
return False