Files
gt-ai-os-community/apps/tenant-backend/app/api/v1/access_groups.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

411 lines
14 KiB
Python

"""
Access Groups API for GT 2.0 Tenant Backend
RESTful API endpoints for managing resource access groups and permissions.
"""
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Header, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db_session
from app.core.security import get_current_user, verify_capability_token
from app.models.access_group import (
AccessGroup, ResourceCreate, ResourceUpdate, ResourceResponse,
AccessGroupModel
)
from app.services.access_controller import AccessController, AccessControlMiddleware
router = APIRouter(
prefix="/api/v1/access-groups",
tags=["access-groups"],
responses={404: {"description": "Not found"}},
)
async def get_access_controller(
x_tenant_domain: str = Header(..., description="Tenant domain")
) -> AccessController:
"""Dependency to get access controller for tenant"""
return AccessController(x_tenant_domain)
async def get_middleware(
x_tenant_domain: str = Header(..., description="Tenant domain")
) -> AccessControlMiddleware:
"""Dependency to get access control middleware"""
return AccessControlMiddleware(x_tenant_domain)
@router.post("/resources", response_model=ResourceResponse)
async def create_resource(
resource: ResourceCreate,
authorization: str = Header(..., description="Bearer token"),
x_tenant_domain: str = Header(..., description="Tenant domain"),
controller: AccessController = Depends(get_access_controller),
db: AsyncSession = Depends(get_db_session)
):
"""
Create a new resource with access control.
- **name**: Resource name
- **resource_type**: Type of resource (agent, dataset, etc.)
- **access_group**: Access level (individual, team, organization)
- **team_members**: List of user IDs for team access
- **metadata**: Resource-specific metadata
"""
try:
# Extract bearer token
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Invalid authorization header")
capability_token = authorization.replace("Bearer ", "")
# Get current user
user = await get_current_user(authorization, db)
# Create resource
created_resource = await controller.create_resource(
user_id=user.id,
resource_data=resource,
capability_token=capability_token
)
return ResourceResponse(
id=created_resource.id,
name=created_resource.name,
resource_type=created_resource.resource_type,
owner_id=created_resource.owner_id,
tenant_domain=created_resource.tenant_domain,
access_group=created_resource.access_group,
team_members=created_resource.team_members,
created_at=created_resource.created_at,
updated_at=created_resource.updated_at,
metadata=created_resource.metadata,
file_path=created_resource.file_path
)
except PermissionError as e:
raise HTTPException(status_code=403, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to create resource: {str(e)}")
@router.put("/resources/{resource_id}/access", response_model=ResourceResponse)
async def update_resource_access(
resource_id: str,
access_update: AccessGroupModel,
authorization: str = Header(..., description="Bearer token"),
x_tenant_domain: str = Header(..., description="Tenant domain"),
controller: AccessController = Depends(get_access_controller),
middleware: AccessControlMiddleware = Depends(get_middleware),
db: AsyncSession = Depends(get_db_session)
):
"""
Update resource access group.
Only the resource owner can change access settings.
- **access_group**: New access level
- **team_members**: Updated team members list (for team access)
"""
try:
# Get current user
user = await get_current_user(authorization, db)
capability_token = authorization.replace("Bearer ", "")
# Verify permission
await middleware.verify_request(
user_id=user.id,
resource_id=resource_id,
action="share",
capability_token=capability_token
)
# Update access
updated_resource = await controller.update_resource_access(
user_id=user.id,
resource_id=resource_id,
new_access_group=access_update.access_group,
team_members=access_update.team_members
)
return ResourceResponse(
id=updated_resource.id,
name=updated_resource.name,
resource_type=updated_resource.resource_type,
owner_id=updated_resource.owner_id,
tenant_domain=updated_resource.tenant_domain,
access_group=updated_resource.access_group,
team_members=updated_resource.team_members,
created_at=updated_resource.created_at,
updated_at=updated_resource.updated_at,
metadata=updated_resource.metadata,
file_path=updated_resource.file_path
)
except PermissionError as e:
raise HTTPException(status_code=403, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to update access: {str(e)}")
@router.get("/resources", response_model=List[ResourceResponse])
async def list_accessible_resources(
resource_type: Optional[str] = Query(None, description="Filter by resource type"),
authorization: str = Header(..., description="Bearer token"),
x_tenant_domain: str = Header(..., description="Tenant domain"),
controller: AccessController = Depends(get_access_controller),
db: AsyncSession = Depends(get_db_session)
):
"""
List all resources accessible to the current user.
Returns resources based on access groups:
- Individual: Only owned resources
- Team: Resources shared with user's teams
- Organization: All organization-wide resources
"""
try:
# Get current user
user = await get_current_user(authorization, db)
# Get accessible resources
resources = await controller.list_accessible_resources(
user_id=user.id,
resource_type=resource_type
)
return [
ResourceResponse(
id=r.id,
name=r.name,
resource_type=r.resource_type,
owner_id=r.owner_id,
tenant_domain=r.tenant_domain,
access_group=r.access_group,
team_members=r.team_members,
created_at=r.created_at,
updated_at=r.updated_at,
metadata=r.metadata,
file_path=r.file_path
)
for r in resources
]
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to list resources: {str(e)}")
@router.get("/resources/{resource_id}", response_model=ResourceResponse)
async def get_resource(
resource_id: str,
authorization: str = Header(..., description="Bearer token"),
x_tenant_domain: str = Header(..., description="Tenant domain"),
controller: AccessController = Depends(get_access_controller),
middleware: AccessControlMiddleware = Depends(get_middleware),
db: AsyncSession = Depends(get_db_session)
):
"""
Get a specific resource if user has access.
Checks read permission based on access group.
"""
try:
# Get current user
user = await get_current_user(authorization, db)
capability_token = authorization.replace("Bearer ", "")
# Verify permission
await middleware.verify_request(
user_id=user.id,
resource_id=resource_id,
action="read",
capability_token=capability_token
)
# Load resource
resource = await controller._load_resource(resource_id)
return ResourceResponse(
id=resource.id,
name=resource.name,
resource_type=resource.resource_type,
owner_id=resource.owner_id,
tenant_domain=resource.tenant_domain,
access_group=resource.access_group,
team_members=resource.team_members,
created_at=resource.created_at,
updated_at=resource.updated_at,
metadata=resource.metadata,
file_path=resource.file_path
)
except PermissionError as e:
raise HTTPException(status_code=403, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get resource: {str(e)}")
@router.delete("/resources/{resource_id}")
async def delete_resource(
resource_id: str,
authorization: str = Header(..., description="Bearer token"),
x_tenant_domain: str = Header(..., description="Tenant domain"),
controller: AccessController = Depends(get_access_controller),
middleware: AccessControlMiddleware = Depends(get_middleware),
db: AsyncSession = Depends(get_db_session)
):
"""
Delete a resource.
Only the resource owner can delete.
"""
try:
# Get current user
user = await get_current_user(authorization, db)
capability_token = authorization.replace("Bearer ", "")
# Verify permission
await middleware.verify_request(
user_id=user.id,
resource_id=resource_id,
action="delete",
capability_token=capability_token
)
# Delete resource (implementation needed)
# await controller.delete_resource(resource_id)
return {"status": "deleted", "resource_id": resource_id}
except PermissionError as e:
raise HTTPException(status_code=403, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to delete resource: {str(e)}")
@router.get("/stats")
async def get_resource_stats(
authorization: str = Header(..., description="Bearer token"),
x_tenant_domain: str = Header(..., description="Tenant domain"),
controller: AccessController = Depends(get_access_controller),
db: AsyncSession = Depends(get_db_session)
):
"""
Get resource statistics for the current user.
Returns counts by type and access group.
"""
try:
# Get current user
user = await get_current_user(authorization, db)
# Get stats
stats = await controller.get_resource_stats(user_id=user.id)
return stats
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get stats: {str(e)}")
@router.post("/resources/{resource_id}/team-members/{user_id}")
async def add_team_member(
resource_id: str,
user_id: str,
authorization: str = Header(..., description="Bearer token"),
x_tenant_domain: str = Header(..., description="Tenant domain"),
controller: AccessController = Depends(get_access_controller),
middleware: AccessControlMiddleware = Depends(get_middleware),
db: AsyncSession = Depends(get_db_session)
):
"""
Add a user to team access for a resource.
Only the resource owner can add team members.
Resource must have team access group.
"""
try:
# Get current user
current_user = await get_current_user(authorization, db)
capability_token = authorization.replace("Bearer ", "")
# Verify permission
await middleware.verify_request(
user_id=current_user.id,
resource_id=resource_id,
action="share",
capability_token=capability_token
)
# Load resource
resource = await controller._load_resource(resource_id)
# Check if team access
if resource.access_group != AccessGroup.TEAM:
raise HTTPException(
status_code=400,
detail="Resource must have team access to add members"
)
# Add team member
resource.add_team_member(user_id)
# Save changes (implementation needed)
# await controller.save_resource(resource)
return {"status": "added", "user_id": user_id, "resource_id": resource_id}
except PermissionError as e:
raise HTTPException(status_code=403, detail=str(e))
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to add team member: {str(e)}")
@router.delete("/resources/{resource_id}/team-members/{user_id}")
async def remove_team_member(
resource_id: str,
user_id: str,
authorization: str = Header(..., description="Bearer token"),
x_tenant_domain: str = Header(..., description="Tenant domain"),
controller: AccessController = Depends(get_access_controller),
middleware: AccessControlMiddleware = Depends(get_middleware),
db: AsyncSession = Depends(get_db_session)
):
"""
Remove a user from team access for a resource.
Only the resource owner can remove team members.
"""
try:
# Get current user
current_user = await get_current_user(authorization, db)
capability_token = authorization.replace("Bearer ", "")
# Verify permission
await middleware.verify_request(
user_id=current_user.id,
resource_id=resource_id,
action="share",
capability_token=capability_token
)
# Load resource
resource = await controller._load_resource(resource_id)
# Remove team member
resource.remove_team_member(user_id)
# Save changes (implementation needed)
# await controller.save_resource(resource)
return {"status": "removed", "user_id": user_id, "resource_id": resource_id}
except PermissionError as e:
raise HTTPException(status_code=403, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to remove team member: {str(e)}")