1→""" 2→Permissions Router 3→================== 4→ 5→API endpoints for permission management within projects. 6→""" 7→ 8→from datetime import datetime 9→from typing import Optional, List 10→ 11→from fastapi import APIRouter, Depends, HTTPException, status 12→from pydantic import BaseModel, Field 13→from sqlalchemy import select, and_ 14→from sqlalchemy.ext.asyncio import AsyncSession 15→ 16→from ..database import get_db 17→from ..models import Project, Permission, PermissionRequest 18→ 19→router = APIRouter(prefix="/api/projects", tags=["permissions"]) 20→ 21→ 22→# ============================================================================ 23→# Pydantic Schemas 24→# ============================================================================ 25→ 26→class PermissionResponse(BaseModel): 27→ """Schema for permission response.""" 28→ id: int 29→ project_id: int 30→ permission_type: str 31→ permission_value: str 32→ granted: bool 33→ created_at: datetime 34→ 35→ class Config: 36→ from_attributes = True 37→ 38→ 39→class PermissionListResponse(BaseModel): 40→ """Schema for permission list response.""" 41→ permissions: List[PermissionResponse] 42→ count: int 43→ 44→ 45→class PermissionCreate(BaseModel): 46→ """Schema for creating a permission.""" 47→ permission_type: str = Field(..., min_length=1, max_length=50) 48→ permission_value: str = Field(..., min_length=1, max_length=500) 49→ granted: bool = True 50→ 51→ 52→class PermissionUpdate(BaseModel): 53→ """Schema for updating a permission.""" 54→ granted: Optional[bool] = None 55→ 56→ 57→class PermissionRequestResponse(BaseModel): 58→ """Schema for permission request response.""" 59→ id: int 60→ project_id: int 61→ feature_id: Optional[int] 62→ agent_id: str 63→ permission_type: str 64→ permission_value: str 65→ status: str 66→ requested_at: datetime 67→ resolved_at: Optional[datetime] 68→ 69→ class Config: 70→ from_attributes = True 71→ 72→ 73→class PermissionRequestListResponse(BaseModel): 74→ """Schema for permission request list response.""" 75→ requests: List[PermissionRequestResponse] 76→ count: int 77→ 78→ 79→class PermissionRequestCreate(BaseModel): 80→ """Schema for creating a permission request.""" 81→ agent_id: str = Field(..., min_length=1, max_length=1) # "A" or "B" 82→ permission_type: str = Field(..., min_length=1, max_length=50) 83→ permission_value: str = Field(..., min_length=1, max_length=500) 84→ feature_id: Optional[int] = None 85→ 86→ 87→class PermissionImportItem(BaseModel): 88→ """Schema for a single permission in import.""" 89→ permission_type: str = Field(..., min_length=1, max_length=50) 90→ permission_value: str = Field(..., min_length=1, max_length=500) 91→ granted: bool = True 92→ 93→ 94→class PermissionImportRequest(BaseModel): 95→ """Schema for importing permissions from configuration.""" 96→ permissions: List[PermissionImportItem] 97→ replace_existing: bool = False # If true, delete all existing permissions first 98→ 99→ 100→class PermissionImportResponse(BaseModel): 101→ """Schema for import response.""" 102→ imported: int 103→ skipped: int 104→ message: str 105→ 106→ 107→class PermissionExportItem(BaseModel): 108→ """Schema for a single permission in export.""" 109→ permission_type: str 110→ permission_value: str 111→ granted: bool 112→ 113→ 114→class PermissionExportResponse(BaseModel): 115→ """Schema for export response.""" 116→ project_name: str 117→ permissions: List[PermissionExportItem] 118→ count: int 119→ 120→ 121→# ============================================================================ 122→# Permission Endpoints 123→# ============================================================================ 124→ 125→@router.get("/{name}/permissions", response_model=PermissionListResponse) 126→async def list_permissions(name: str, db: AsyncSession = Depends(get_db)): 127→ """List all permissions for a project.""" 128→ # Verify project exists 129→ result = await db.execute(select(Project).where(Project.name == name)) 130→ project = result.scalar_one_or_none() 131→ 132→ if not project: 133→ raise HTTPException( 134→ status_code=status.HTTP_404_NOT_FOUND, 135→ detail=f"Project '{name}' not found" 136→ ) 137→ 138→ # Get permissions 139→ result = await db.execute( 140→ select(Permission) 141→ .where(Permission.project_id == project.id) 142→ .order_by(Permission.created_at.desc()) 143→ ) 144→ permissions = result.scalars().all() 145→ 146→ return PermissionListResponse( 147→ permissions=[PermissionResponse.model_validate(p) for p in permissions], 148→ count=len(permissions) 149→ ) 150→ 151→ 152→@router.post("/{name}/permissions", response_model=PermissionResponse, status_code=status.HTTP_201_CREATED) 153→async def create_permission(name: str, permission: PermissionCreate, db: AsyncSession = Depends(get_db)): 154→ """Create a new permission for a project.""" 155→ # Verify project exists 156→ result = await db.execute(select(Project).where(Project.name == name)) 157→ project = result.scalar_one_or_none() 158→ 159→ if not project: 160→ raise HTTPException( 161→ status_code=status.HTTP_404_NOT_FOUND, 162→ detail=f"Project '{name}' not found" 163→ ) 164→ 165→ # Check if permission already exists 166→ result = await db.execute( 167→ select(Permission).where(and_( 168→ Permission.project_id == project.id, 169→ Permission.permission_type == permission.permission_type, 170→ Permission.permission_value == permission.permission_value 171→ )) 172→ ) 173→ existing = result.scalar_one_or_none() 174→ if existing: 175→ raise HTTPException( 176→ status_code=status.HTTP_409_CONFLICT, 177→ detail=f"Permission already exists" 178→ ) 179→ 180→ # Create permission 181→ db_permission = Permission( 182→ project_id=project.id, 183→ permission_type=permission.permission_type, 184→ permission_value=permission.permission_value, 185→ granted=permission.granted, 186→ ) 187→ db.add(db_permission) 188→ await db.commit() 189→ await db.refresh(db_permission) 190→ 191→ return PermissionResponse.model_validate(db_permission) 192→ 193→ 194→# ============================================================================ 195→# Permission Import/Export Endpoints (must be before /{permission_id} routes) 196→# ============================================================================ 197→ 198→@router.get("/{name}/permissions/export", response_model=PermissionExportResponse) 199→async def export_permissions(name: str, db: AsyncSession = Depends(get_db)): 200→ """Export permissions to a configuration format.""" 201→ # Verify project exists 202→ result = await db.execute(select(Project).where(Project.name == name)) 203→ project = result.scalar_one_or_none() 204→ 205→ if not project: 206→ raise HTTPException( 207→ status_code=status.HTTP_404_NOT_FOUND, 208→ detail=f"Project '{name}' not found" 209→ ) 210→ 211→ # Get all permissions 212→ result = await db.execute( 213→ select(Permission) 214→ .where(Permission.project_id == project.id) 215→ .order_by(Permission.permission_type, Permission.permission_value) 216→ ) 217→ permissions = result.scalars().all() 218→ 219→ return PermissionExportResponse( 220→ project_name=name, 221→ permissions=[ 222→ PermissionExportItem( 223→ permission_type=p.permission_type, 224→ permission_value=p.permission_value, 225→ granted=p.granted 226→ ) 227→ for p in permissions 228→ ], 229→ count=len(permissions) 230→ ) 231→ 232→ 233→@router.post("/{name}/permissions/import", response_model=PermissionImportResponse) 234→async def import_permissions(name: str, import_data: PermissionImportRequest, db: AsyncSession = Depends(get_db)): 235→ """Import permissions from a configuration file.""" 236→ # Verify project exists 237→ result = await db.execute(select(Project).where(Project.name == name)) 238→ project = result.scalar_one_or_none() 239→ 240→ if not project: 241→ raise HTTPException( 242→ status_code=status.HTTP_404_NOT_FOUND, 243→ detail=f"Project '{name}' not found" 244→ ) 245→ 246→ imported = 0 247→ skipped = 0 248→ 249→ # If replace_existing is true, delete all existing permissions first 250→ if import_data.replace_existing: 251→ existing_result = await db.execute( 252→ select(Permission).where(Permission.project_id == project.id) 253→ ) 254→ existing_permissions = existing_result.scalars().all() 255→ for perm in existing_permissions: 256→ await db.delete(perm) 257→ 258→ # Import each permission 259→ for perm in import_data.permissions: 260→ # Check if permission already exists (skip duplicates) 261→ result = await db.execute( 262→ select(Permission).where(and_( 263→ Permission.project_id == project.id, 264→ Permission.permission_type == perm.permission_type, 265→ Permission.permission_value == perm.permission_value 266→ )) 267→ ) 268→ existing = result.scalar_one_or_none() 269→ 270→ if existing: 271→ skipped += 1 272→ continue 273→ 274→ # Create new permission 275→ db_permission = Permission( 276→ project_id=project.id, 277→ permission_type=perm.permission_type, 278→ permission_value=perm.permission_value, 279→ granted=perm.granted, 280→ ) 281→ db.add(db_permission) 282→ imported += 1 283→ 284→ await db.commit() 285→ 286→ return PermissionImportResponse( 287→ imported=imported, 288→ skipped=skipped, 289→ message=f"Successfully imported {imported} permissions ({skipped} skipped as duplicates)" 290→ ) 291→ 292→ 293→# ============================================================================ 294→# Permission ID Endpoints 295→# ============================================================================ 296→ 297→@router.get("/{name}/permissions/{permission_id}", response_model=PermissionResponse) 298→async def get_permission(name: str, permission_id: int, db: AsyncSession = Depends(get_db)): 299→ """Get a specific permission by ID.""" 300→ # Verify project exists 301→ result = await db.execute(select(Project).where(Project.name == name)) 302→ project = result.scalar_one_or_none() 303→ 304→ if not project: 305→ raise HTTPException( 306→ status_code=status.HTTP_404_NOT_FOUND, 307→ detail=f"Project '{name}' not found" 308→ ) 309→ 310→ # Get permission 311→ result = await db.execute( 312→ select(Permission).where(and_( 313→ Permission.id == permission_id, 314→ Permission.project_id == project.id 315→ )) 316→ ) 317→ permission = result.scalar_one_or_none() 318→ 319→ if not permission: 320→ raise HTTPException( 321→ status_code=status.HTTP_404_NOT_FOUND, 322→ detail=f"Permission {permission_id} not found in project '{name}'" 323→ ) 324→ 325→ return PermissionResponse.model_validate(permission) 326→ 327→ 328→@router.put("/{name}/permissions/{permission_id}", response_model=PermissionResponse) 329→async def update_permission(name: str, permission_id: int, permission_update: PermissionUpdate, db: AsyncSession = Depends(get_db)): 330→ """Update a permission.""" 331→ # Verify project exists 332→ result = await db.execute(select(Project).where(Project.name == name)) 333→ project = result.scalar_one_or_none() 334→ 335→ if not project: 336→ raise HTTPException( 337→ status_code=status.HTTP_404_NOT_FOUND, 338→ detail=f"Project '{name}' not found" 339→ ) 340→ 341→ # Get permission 342→ result = await db.execute( 343→ select(Permission).where(and_( 344→ Permission.id == permission_id, 345→ Permission.project_id == project.id 346→ )) 347→ ) 348→ permission = result.scalar_one_or_none() 349→ 350→ if not permission: 351→ raise HTTPException( 352→ status_code=status.HTTP_404_NOT_FOUND, 353→ detail=f"Permission {permission_id} not found in project '{name}'" 354→ ) 355→ 356→ # Update granted status 357→ if permission_update.granted is not None: 358→ permission.granted = permission_update.granted 359→ 360→ await db.commit() 361→ await db.refresh(permission) 362→ 363→ return PermissionResponse.model_validate(permission) 364→ 365→ 366→@router.delete("/{name}/permissions/{permission_id}", status_code=status.HTTP_204_NO_CONTENT) 367→async def delete_permission(name: str, permission_id: int, db: AsyncSession = Depends(get_db)): 368→ """Delete a permission.""" 369→ # Verify project exists 370→ result = await db.execute(select(Project).where(Project.name == name)) 371→ project = result.scalar_one_or_none() 372→ 373→ if not project: 374→ raise HTTPException( 375→ status_code=status.HTTP_404_NOT_FOUND, 376→ detail=f"Project '{name}' not found" 377→ ) 378→ 379→ # Get permission 380→ result = await db.execute( 381→ select(Permission).where(and_( 382→ Permission.id == permission_id, 383→ Permission.project_id == project.id 384→ )) 385→ ) 386→ permission = result.scalar_one_or_none() 387→ 388→ if not permission: 389→ raise HTTPException( 390→ status_code=status.HTTP_404_NOT_FOUND, 391→ detail=f"Permission {permission_id} not found in project '{name}'" 392→ ) 393→ 394→ await db.delete(permission) 395→ await db.commit() 396→ 397→ 398→# ============================================================================ 399→# Permission Request Endpoints 400→# ============================================================================ 401→ 402→@router.get("/{name}/permission-requests", response_model=PermissionRequestListResponse) 403→async def list_permission_requests( 404→ name: str, 405→ status_filter: Optional[str] = None, 406→ db: AsyncSession = Depends(get_db) 407→): 408→ """List permission requests for a project.""" 409→ # Verify project exists 410→ result = await db.execute(select(Project).where(Project.name == name)) 411→ project = result.scalar_one_or_none() 412→ 413→ if not project: 414→ raise HTTPException( 415→ status_code=status.HTTP_404_NOT_FOUND, 416→ detail=f"Project '{name}' not found" 417→ ) 418→ 419→ # Build query 420→ query = select(PermissionRequest).where(PermissionRequest.project_id == project.id) 421→ 422→ if status_filter: 423→ query = query.where(PermissionRequest.status == status_filter) 424→ 425→ query = query.order_by(PermissionRequest.requested_at.desc()) 426→ 427→ result = await db.execute(query) 428→ requests = result.scalars().all() 429→ 430→ return PermissionRequestListResponse( 431→ requests=[PermissionRequestResponse.model_validate(r) for r in requests], 432→ count=len(requests) 433→ ) 434→ 435→ 436→@router.post("/{name}/permission-requests", response_model=PermissionRequestResponse, status_code=status.HTTP_201_CREATED) 437→async def create_permission_request(name: str, request: PermissionRequestCreate, db: AsyncSession = Depends(get_db)): 438→ """Create a new permission request (typically called by agents).""" 439→ # Verify project exists 440→ result = await db.execute(select(Project).where(Project.name == name)) 441→ project = result.scalar_one_or_none() 442→ 443→ if not project: 444→ raise HTTPException( 445→ status_code=status.HTTP_404_NOT_FOUND, 446→ detail=f"Project '{name}' not found" 447→ ) 448→ 449→ # Create permission request 450→ db_request = PermissionRequest( 451→ project_id=project.id, 452→ feature_id=request.feature_id, 453→ agent_id=request.agent_id, 454→ permission_type=request.permission_type, 455→ permission_value=request.permission_value, 456→ status="pending", 457→ ) 458→ db.add(db_request) 459→ await db.commit() 460→ await db.refresh(db_request) 461→ 462→ return PermissionRequestResponse.model_validate(db_request) 463→ 464→ 465→@router.post("/{name}/permission-requests/{request_id}/grant", response_model=PermissionRequestResponse) 466→async def grant_permission_request(name: str, request_id: int, db: AsyncSession = Depends(get_db)): 467→ """Grant a pending permission request.""" 468→ # Verify project exists 469→ result = await db.execute(select(Project).where(Project.name == name)) 470→ project = result.scalar_one_or_none() 471→ 472→ if not project: 473→ raise HTTPException( 474→ status_code=status.HTTP_404_NOT_FOUND, 475→ detail=f"Project '{name}' not found" 476→ ) 477→ 478→ # Get permission request 479→ result = await db.execute( 480→ select(PermissionRequest).where(and_( 481→ PermissionRequest.id == request_id, 482→ PermissionRequest.project_id == project.id 483→ )) 484→ ) 485→ request = result.scalar_one_or_none() 486→ 487→ if not request: 488→ raise HTTPException( 489→ status_code=status.HTTP_404_NOT_FOUND, 490→ detail=f"Permission request {request_id} not found" 491→ ) 492→ 493→ if request.status != "pending": 494→ raise HTTPException( 495→ status_code=status.HTTP_400_BAD_REQUEST, 496→ detail=f"Permission request is already {request.status}" 497→ ) 498→ 499→ # Update request status 500→ request.status = "granted" 501→ request.resolved_at = datetime.utcnow() 502→ 503→ # Create the actual permission 504→ db_permission = Permission( 505→ project_id=project.id, 506→ permission_type=request.permission_type, 507→ permission_value=request.permission_value, 508→ granted=True, 509→ ) 510→ db.add(db_permission) 511→ 512→ await db.commit() 513→ await db.refresh(request) 514→ 515→ return PermissionRequestResponse.model_validate(request) 516→ 517→ 518→@router.post("/{name}/permission-requests/{request_id}/deny", response_model=PermissionRequestResponse) 519→async def deny_permission_request(name: str, request_id: int, db: AsyncSession = Depends(get_db)): 520→ """Deny a pending permission request.""" 521→ # Verify project exists 522→ result = await db.execute(select(Project).where(Project.name == name)) 523→ project = result.scalar_one_or_none() 524→ 525→ if not project: 526→ raise HTTPException( 527→ status_code=status.HTTP_404_NOT_FOUND, 528→ detail=f"Project '{name}' not found" 529→ ) 530→ 531→ # Get permission request 532→ result = await db.execute( 533→ select(PermissionRequest).where(and_( 534→ PermissionRequest.id == request_id, 535→ PermissionRequest.project_id == project.id 536→ )) 537→ ) 538→ request = result.scalar_one_or_none() 539→ 540→ if not request: 541→ raise HTTPException( 542→ status_code=status.HTTP_404_NOT_FOUND, 543→ detail=f"Permission request {request_id} not found" 544→ ) 545→ 546→ if request.status != "pending": 547→ raise HTTPException( 548→ status_code=status.HTTP_400_BAD_REQUEST, 549→ detail=f"Permission request is already {request.status}" 550→ ) 551→ 552→ # Update request status 553→ request.status = "denied" 554→ request.resolved_at = datetime.utcnow() 555→ 556→ await db.commit() 557→ await db.refresh(request) 558→ 559→ return PermissionRequestResponse.model_validate(request) 560→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.