feat(06.2-02): backend — ShareCreate.permission field + PATCH /{share_id} endpoint
- Add permission field (default "view") with field_validator to ShareCreate
- Add SharePermissionPatch model with same validator
- Wire body.permission into grant_share() Share constructor
- Add PATCH /{share_id} endpoint with IDOR protection (T-06.2-02-01)
- Promote 3 xfail stubs to real tests (create_with_permission, patch_permission, patch_idor)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+51
-2
@@ -19,7 +19,7 @@ import uuid
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, field_validator
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -38,6 +38,25 @@ router = APIRouter(prefix="/api/shares", tags=["shares"])
|
||||
class ShareCreate(BaseModel):
|
||||
document_id: str
|
||||
recipient_handle: str
|
||||
permission: str = "view"
|
||||
|
||||
@field_validator("permission")
|
||||
@classmethod
|
||||
def validate_permission(cls, v: str) -> str:
|
||||
if v not in {"view", "edit"}:
|
||||
raise ValueError("permission must be 'view' or 'edit'")
|
||||
return v
|
||||
|
||||
|
||||
class SharePermissionPatch(BaseModel):
|
||||
permission: str
|
||||
|
||||
@field_validator("permission")
|
||||
@classmethod
|
||||
def validate_permission(cls, v: str) -> str:
|
||||
if v not in {"view", "edit"}:
|
||||
raise ValueError("permission must be 'view' or 'edit'")
|
||||
return v
|
||||
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
@@ -94,7 +113,7 @@ async def grant_share(
|
||||
document_id=uid,
|
||||
owner_id=current_user.id,
|
||||
recipient_id=recipient.id,
|
||||
permission="view",
|
||||
permission=body.permission,
|
||||
)
|
||||
session.add(share)
|
||||
|
||||
@@ -221,6 +240,36 @@ async def list_shared_with_me(
|
||||
return {"items": items}
|
||||
|
||||
|
||||
# ── PATCH /api/shares/{share_id} ─────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.patch("/{share_id}", status_code=200)
|
||||
async def update_share_permission(
|
||||
share_id: str,
|
||||
body: SharePermissionPatch,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_regular_user),
|
||||
) -> dict:
|
||||
"""Update the permission on an existing share (SHARE-03, D-09).
|
||||
|
||||
T-06.2-02-01 IDOR protection: 404 on owner mismatch — mirrors revoke_share exactly.
|
||||
T-06.2-02-02: SharePermissionPatch validator prevents arbitrary string passthrough.
|
||||
"""
|
||||
try:
|
||||
sid = uuid.UUID(share_id)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=404, detail="Share not found")
|
||||
|
||||
share = await session.get(Share, sid)
|
||||
if share is None or share.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=404, detail="Share not found")
|
||||
|
||||
share.permission = body.permission
|
||||
await session.commit()
|
||||
|
||||
return {"id": str(share.id), "permission": share.permission}
|
||||
|
||||
|
||||
# ── DELETE /api/shares/{share_id} ─────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user