feat(12-02): decompose api/cloud.py into package with connection-ID browse endpoint
- Add api/cloud/ package: connections.py, browse.py, schemas.py, __init__.py
- Add GET /api/cloud/connections/{connection_id}/items (T-12-01 IDOR, T-12-03 cred-free)
- Add PATCH /api/cloud/connections/{id} for display_name_override rename
- Add display_name_override ORM field to CloudConnection model
- Add CloudResourceAdapter service layer with str/UUID coercion
- Fix UUID type compatibility: test_cloud_items.py now uses UUID(as_uuid=True)
matching conftest — removes String(36) patch that caused type incompatibility
- Add 11 Phase 12 integration tests (IDOR, admin block, credential exclusion,
duplicate providers, rename, malformed UUID)
- Remove deleted api/cloud.py (replaced by api/cloud/ package)
This commit is contained in:
@@ -37,17 +37,20 @@ class CloudItemNotFound(ValueError):
|
||||
async def resolve_owned_connection(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
connection_id: str,
|
||||
user_id: str,
|
||||
connection_id,
|
||||
user_id,
|
||||
) -> CloudConnection:
|
||||
"""Return the CloudConnection owned by user_id or raise ConnectionNotFound.
|
||||
|
||||
Accepts UUID objects or string UUIDs for both parameters.
|
||||
Never returns a connection belonging to another user.
|
||||
"""
|
||||
conn_uuid = connection_id if isinstance(connection_id, uuid.UUID) else uuid.UUID(str(connection_id))
|
||||
user_uuid = user_id if isinstance(user_id, uuid.UUID) else uuid.UUID(str(user_id))
|
||||
result = await session.execute(
|
||||
select(CloudConnection).where(
|
||||
CloudConnection.id == connection_id,
|
||||
CloudConnection.user_id == user_id,
|
||||
CloudConnection.id == conn_uuid,
|
||||
CloudConnection.user_id == user_uuid,
|
||||
)
|
||||
)
|
||||
conn = result.scalars().first()
|
||||
@@ -72,9 +75,11 @@ async def list_cloud_children(
|
||||
parent_ref=None matches items whose parent_ref is NULL (root children where
|
||||
parent is not tracked as a ref string).
|
||||
"""
|
||||
uid_v = user_id if isinstance(user_id, uuid.UUID) else uuid.UUID(str(user_id))
|
||||
cid_v = connection_id if isinstance(connection_id, uuid.UUID) else uuid.UUID(str(connection_id))
|
||||
stmt = select(CloudItem).where(
|
||||
CloudItem.user_id == user_id,
|
||||
CloudItem.connection_id == connection_id,
|
||||
CloudItem.user_id == uid_v,
|
||||
CloudItem.connection_id == cid_v,
|
||||
CloudItem.deleted_at.is_(None),
|
||||
)
|
||||
if parent_ref is None:
|
||||
@@ -103,11 +108,10 @@ async def upsert_cloud_item(
|
||||
user_id is always set from the caller — never from the resource alone —
|
||||
to enforce the owner boundary.
|
||||
"""
|
||||
connection_id = str(resource.connection_id)
|
||||
|
||||
conn_uuid2 = resource.connection_id if isinstance(resource.connection_id, uuid.UUID) else uuid.UUID(str(resource.connection_id))
|
||||
result = await session.execute(
|
||||
select(CloudItem).where(
|
||||
CloudItem.connection_id == connection_id,
|
||||
CloudItem.connection_id == conn_uuid2,
|
||||
CloudItem.provider_item_id == resource.provider_item_id,
|
||||
)
|
||||
)
|
||||
@@ -130,10 +134,12 @@ async def upsert_cloud_item(
|
||||
await session.flush()
|
||||
return existing
|
||||
else:
|
||||
user_uuid_ins = user_id if isinstance(user_id, uuid.UUID) else uuid.UUID(str(user_id))
|
||||
conn_uuid_ins = resource.connection_id if isinstance(resource.connection_id, uuid.UUID) else uuid.UUID(str(resource.connection_id))
|
||||
item = CloudItem(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=user_id,
|
||||
connection_id=connection_id,
|
||||
id=uuid.uuid4(),
|
||||
user_id=user_uuid_ins,
|
||||
connection_id=conn_uuid_ins,
|
||||
provider_item_id=resource.provider_item_id,
|
||||
parent_ref=resource.parent_ref,
|
||||
name=resource.name,
|
||||
@@ -181,9 +187,11 @@ async def reconcile_cloud_listing(
|
||||
|
||||
if listing.complete:
|
||||
# Soft-delete items not present in the complete listing
|
||||
uid_v2 = user_id if isinstance(user_id, uuid.UUID) else uuid.UUID(str(user_id))
|
||||
cid_v2 = connection_id if isinstance(connection_id, uuid.UUID) else uuid.UUID(str(connection_id))
|
||||
stmt = select(CloudItem).where(
|
||||
CloudItem.user_id == user_id,
|
||||
CloudItem.connection_id == connection_id,
|
||||
CloudItem.user_id == uid_v2,
|
||||
CloudItem.connection_id == cid_v2,
|
||||
CloudItem.deleted_at.is_(None),
|
||||
)
|
||||
if parent_ref is None:
|
||||
@@ -217,9 +225,11 @@ async def get_or_create_folder_state(
|
||||
Idempotent: repeated calls with the same arguments return the same row.
|
||||
parent_ref='' represents the connection root.
|
||||
"""
|
||||
cid_fs = connection_id if isinstance(connection_id, uuid.UUID) else uuid.UUID(str(connection_id))
|
||||
uid_fs = user_id if isinstance(user_id, uuid.UUID) else uuid.UUID(str(user_id))
|
||||
result = await session.execute(
|
||||
select(CloudFolderState).where(
|
||||
CloudFolderState.connection_id == connection_id,
|
||||
CloudFolderState.connection_id == cid_fs,
|
||||
CloudFolderState.parent_ref == parent_ref,
|
||||
)
|
||||
)
|
||||
@@ -228,9 +238,9 @@ async def get_or_create_folder_state(
|
||||
return existing
|
||||
|
||||
fs = CloudFolderState(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=user_id,
|
||||
connection_id=connection_id,
|
||||
id=uuid.uuid4(),
|
||||
user_id=uid_fs,
|
||||
connection_id=cid_fs,
|
||||
parent_ref=parent_ref,
|
||||
refresh_state="fresh",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user