@weirdfingers/baseboards 0.9.5 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +561 -469
- package/dist/index.js.map +1 -1
- package/package.json +2 -5
- package/templates/README.md +0 -122
- package/templates/api/.env.example +0 -65
- package/templates/api/ARTIFACT_RESOLUTION_GUIDE.md +0 -148
- package/templates/api/Dockerfile +0 -32
- package/templates/api/README.md +0 -264
- package/templates/api/alembic/env.py +0 -114
- package/templates/api/alembic/script.py.mako +0 -28
- package/templates/api/alembic/versions/20250101_000000_initial_schema.py +0 -506
- package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +0 -75
- package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +0 -467
- package/templates/api/alembic/versions/20251202_000000_add_artifact_lineage.py +0 -134
- package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +0 -88
- package/templates/api/alembic.ini +0 -36
- package/templates/api/config/generators.yaml +0 -237
- package/templates/api/config/storage_config.yaml +0 -26
- package/templates/api/docs/ADDING_GENERATORS.md +0 -409
- package/templates/api/docs/GENERATORS_API.md +0 -502
- package/templates/api/docs/MIGRATIONS.md +0 -472
- package/templates/api/docs/TESTING_LIVE_APIS.md +0 -417
- package/templates/api/docs/storage_providers.md +0 -337
- package/templates/api/pyproject.toml +0 -205
- package/templates/api/src/boards/__init__.py +0 -10
- package/templates/api/src/boards/api/app.py +0 -172
- package/templates/api/src/boards/api/auth.py +0 -75
- package/templates/api/src/boards/api/endpoints/__init__.py +0 -3
- package/templates/api/src/boards/api/endpoints/jobs.py +0 -76
- package/templates/api/src/boards/api/endpoints/setup.py +0 -505
- package/templates/api/src/boards/api/endpoints/sse.py +0 -129
- package/templates/api/src/boards/api/endpoints/storage.py +0 -155
- package/templates/api/src/boards/api/endpoints/tenant_registration.py +0 -296
- package/templates/api/src/boards/api/endpoints/uploads.py +0 -149
- package/templates/api/src/boards/api/endpoints/webhooks.py +0 -13
- package/templates/api/src/boards/auth/__init__.py +0 -15
- package/templates/api/src/boards/auth/adapters/__init__.py +0 -27
- package/templates/api/src/boards/auth/adapters/auth0.py +0 -220
- package/templates/api/src/boards/auth/adapters/base.py +0 -73
- package/templates/api/src/boards/auth/adapters/clerk.py +0 -172
- package/templates/api/src/boards/auth/adapters/jwt.py +0 -122
- package/templates/api/src/boards/auth/adapters/none.py +0 -102
- package/templates/api/src/boards/auth/adapters/oidc.py +0 -284
- package/templates/api/src/boards/auth/adapters/supabase.py +0 -110
- package/templates/api/src/boards/auth/context.py +0 -35
- package/templates/api/src/boards/auth/factory.py +0 -129
- package/templates/api/src/boards/auth/middleware.py +0 -221
- package/templates/api/src/boards/auth/provisioning.py +0 -129
- package/templates/api/src/boards/auth/tenant_extraction.py +0 -278
- package/templates/api/src/boards/cli.py +0 -354
- package/templates/api/src/boards/config.py +0 -131
- package/templates/api/src/boards/database/__init__.py +0 -7
- package/templates/api/src/boards/database/cli.py +0 -110
- package/templates/api/src/boards/database/connection.py +0 -292
- package/templates/api/src/boards/database/models.py +0 -19
- package/templates/api/src/boards/database/seed_data.py +0 -182
- package/templates/api/src/boards/dbmodels/__init__.py +0 -441
- package/templates/api/src/boards/generators/__init__.py +0 -57
- package/templates/api/src/boards/generators/artifact_resolution.py +0 -405
- package/templates/api/src/boards/generators/artifacts.py +0 -53
- package/templates/api/src/boards/generators/base.py +0 -144
- package/templates/api/src/boards/generators/implementations/__init__.py +0 -14
- package/templates/api/src/boards/generators/implementations/fal/__init__.py +0 -25
- package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +0 -23
- package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_music_generation.py +0 -171
- package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_sound_effect_generation.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_text_to_speech.py +0 -176
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_tts_turbo.py +0 -195
- package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_sound_effects_v2.py +0 -194
- package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_tts_eleven_v3.py +0 -209
- package/templates/api/src/boards/generators/implementations/fal/audio/fal_elevenlabs_tts_turbo_v2_5.py +0 -206
- package/templates/api/src/boards/generators/implementations/fal/audio/fal_minimax_speech_26_hd.py +0 -237
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_music_v2.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +0 -63
- package/templates/api/src/boards/generators/implementations/fal/image/bytedance_seedream_v45_edit.py +0 -219
- package/templates/api/src/boards/generators/implementations/fal/image/clarity_upscaler.py +0 -220
- package/templates/api/src/boards/generators/implementations/fal/image/crystal_upscaler.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/image/fal_ideogram_character.py +0 -227
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2.py +0 -203
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_edit.py +0 -230
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro.py +0 -204
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro_edit.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_kontext.py +0 -216
- package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_ultra.py +0 -197
- package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image.py +0 -177
- package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image_edit.py +0 -208
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_15_edit.py +0 -216
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_5.py +0 -177
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_edit_image.py +0 -182
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_mini.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/image/ideogram_character_edit.py +0 -299
- package/templates/api/src/boards/generators/implementations/fal/image/ideogram_v2.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview.py +0 -191
- package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview_fast.py +0 -179
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana.py +0 -183
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_edit.py +0 -212
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro.py +0 -179
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro_edit.py +0 -226
- package/templates/api/src/boards/generators/implementations/fal/image/qwen_image.py +0 -249
- package/templates/api/src/boards/generators/implementations/fal/image/qwen_image_edit.py +0 -244
- package/templates/api/src/boards/generators/implementations/fal/image/reve_edit.py +0 -178
- package/templates/api/src/boards/generators/implementations/fal/image/reve_text_to_image.py +0 -155
- package/templates/api/src/boards/generators/implementations/fal/image/seedream_v45_text_to_image.py +0 -180
- package/templates/api/src/boards/generators/implementations/fal/utils.py +0 -61
- package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +0 -77
- package/templates/api/src/boards/generators/implementations/fal/video/bytedance_seedance_v1_pro_text_to_video.py +0 -209
- package/templates/api/src/boards/generators/implementations/fal/video/creatify_lipsync.py +0 -161
- package/templates/api/src/boards/generators/implementations/fal/video/fal_bytedance_seedance_v1_pro_image_to_video.py +0 -222
- package/templates/api/src/boards/generators/implementations/fal/video/fal_minimax_hailuo_02_standard_text_to_video.py +0 -152
- package/templates/api/src/boards/generators/implementations/fal/video/fal_pixverse_lipsync.py +0 -197
- package/templates/api/src/boards/generators/implementations/fal/video/fal_sora_2_text_to_video.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/video/infinitalk.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_pro.py +0 -168
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_standard.py +0 -159
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_image_to_video.py +0 -175
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_text_to_video.py +0 -168
- package/templates/api/src/boards/generators/implementations/fal/video/minimax_hailuo_2_3_pro_image_to_video.py +0 -153
- package/templates/api/src/boards/generators/implementations/fal/video/sora2_image_to_video.py +0 -172
- package/templates/api/src/boards/generators/implementations/fal/video/sora_2_image_to_video_pro.py +0 -175
- package/templates/api/src/boards/generators/implementations/fal/video/sora_2_text_to_video_pro.py +0 -163
- package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2_pro.py +0 -155
- package/templates/api/src/boards/generators/implementations/fal/video/veed_fabric_1_0.py +0 -180
- package/templates/api/src/boards/generators/implementations/fal/video/veed_lipsync.py +0 -174
- package/templates/api/src/boards/generators/implementations/fal/video/veo3.py +0 -194
- package/templates/api/src/boards/generators/implementations/fal/video/veo31.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast_image_to_video.py +0 -191
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +0 -187
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_image_to_video.py +0 -183
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_reference_to_video.py +0 -172
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_image_to_video.py +0 -212
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_text_to_video.py +0 -208
- package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py +0 -158
- package/templates/api/src/boards/generators/implementations/kie/__init__.py +0 -11
- package/templates/api/src/boards/generators/implementations/kie/base.py +0 -316
- package/templates/api/src/boards/generators/implementations/kie/image/__init__.py +0 -3
- package/templates/api/src/boards/generators/implementations/kie/image/nano_banana_edit.py +0 -190
- package/templates/api/src/boards/generators/implementations/kie/utils.py +0 -98
- package/templates/api/src/boards/generators/implementations/kie/video/__init__.py +0 -8
- package/templates/api/src/boards/generators/implementations/kie/video/veo3.py +0 -161
- package/templates/api/src/boards/generators/implementations/openai/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/audio/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/audio/whisper.py +0 -69
- package/templates/api/src/boards/generators/implementations/openai/image/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/image/dalle3.py +0 -96
- package/templates/api/src/boards/generators/implementations/replicate/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/image/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/image/flux_pro.py +0 -88
- package/templates/api/src/boards/generators/implementations/replicate/video/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/video/lipsync.py +0 -73
- package/templates/api/src/boards/generators/loader.py +0 -253
- package/templates/api/src/boards/generators/registry.py +0 -114
- package/templates/api/src/boards/generators/resolution.py +0 -632
- package/templates/api/src/boards/generators/testmods/class_gen.py +0 -34
- package/templates/api/src/boards/generators/testmods/import_side_effect.py +0 -35
- package/templates/api/src/boards/graphql/__init__.py +0 -7
- package/templates/api/src/boards/graphql/access_control.py +0 -136
- package/templates/api/src/boards/graphql/mutations/root.py +0 -148
- package/templates/api/src/boards/graphql/queries/root.py +0 -116
- package/templates/api/src/boards/graphql/resolvers/__init__.py +0 -8
- package/templates/api/src/boards/graphql/resolvers/auth.py +0 -12
- package/templates/api/src/boards/graphql/resolvers/board.py +0 -1053
- package/templates/api/src/boards/graphql/resolvers/generation.py +0 -666
- package/templates/api/src/boards/graphql/resolvers/generator.py +0 -50
- package/templates/api/src/boards/graphql/resolvers/lineage.py +0 -381
- package/templates/api/src/boards/graphql/resolvers/upload.py +0 -463
- package/templates/api/src/boards/graphql/resolvers/user.py +0 -25
- package/templates/api/src/boards/graphql/schema.py +0 -81
- package/templates/api/src/boards/graphql/types/board.py +0 -102
- package/templates/api/src/boards/graphql/types/generation.py +0 -166
- package/templates/api/src/boards/graphql/types/generator.py +0 -17
- package/templates/api/src/boards/graphql/types/user.py +0 -47
- package/templates/api/src/boards/jobs/repository.py +0 -153
- package/templates/api/src/boards/logging.py +0 -195
- package/templates/api/src/boards/middleware.py +0 -339
- package/templates/api/src/boards/progress/__init__.py +0 -4
- package/templates/api/src/boards/progress/models.py +0 -25
- package/templates/api/src/boards/progress/publisher.py +0 -64
- package/templates/api/src/boards/py.typed +0 -0
- package/templates/api/src/boards/redis_pool.py +0 -118
- package/templates/api/src/boards/storage/__init__.py +0 -52
- package/templates/api/src/boards/storage/base.py +0 -363
- package/templates/api/src/boards/storage/config.py +0 -187
- package/templates/api/src/boards/storage/factory.py +0 -288
- package/templates/api/src/boards/storage/implementations/__init__.py +0 -27
- package/templates/api/src/boards/storage/implementations/gcs.py +0 -340
- package/templates/api/src/boards/storage/implementations/local.py +0 -201
- package/templates/api/src/boards/storage/implementations/s3.py +0 -294
- package/templates/api/src/boards/storage/implementations/supabase.py +0 -218
- package/templates/api/src/boards/tenant_isolation.py +0 -446
- package/templates/api/src/boards/validation.py +0 -262
- package/templates/api/src/boards/workers/__init__.py +0 -1
- package/templates/api/src/boards/workers/actors.py +0 -274
- package/templates/api/src/boards/workers/cli.py +0 -125
- package/templates/api/src/boards/workers/context.py +0 -348
- package/templates/api/src/boards/workers/middleware.py +0 -58
- package/templates/api/src/py.typed +0 -0
- package/templates/compose.web.yaml +0 -35
- package/templates/compose.yaml +0 -116
- package/templates/docker/env.example +0 -23
- package/templates/web/.env.example +0 -28
- package/templates/web/Dockerfile +0 -51
- package/templates/web/components.json +0 -22
- package/templates/web/imageLoader.js +0 -18
- package/templates/web/next-env.d.ts +0 -5
- package/templates/web/next.config.js +0 -36
- package/templates/web/package.json +0 -41
- package/templates/web/postcss.config.mjs +0 -7
- package/templates/web/public/favicon.ico +0 -0
- package/templates/web/src/app/boards/[boardId]/page.tsx +0 -353
- package/templates/web/src/app/globals.css +0 -123
- package/templates/web/src/app/layout.tsx +0 -31
- package/templates/web/src/app/lineage/[generationId]/page.tsx +0 -235
- package/templates/web/src/app/page.tsx +0 -35
- package/templates/web/src/app/providers.tsx +0 -18
- package/templates/web/src/components/boards/ArtifactInputSlots.tsx +0 -206
- package/templates/web/src/components/boards/ArtifactPreview.tsx +0 -466
- package/templates/web/src/components/boards/GenerationGrid.tsx +0 -282
- package/templates/web/src/components/boards/GenerationInput.tsx +0 -370
- package/templates/web/src/components/boards/GeneratorSelector.tsx +0 -272
- package/templates/web/src/components/boards/UploadArtifact.tsx +0 -563
- package/templates/web/src/components/header.tsx +0 -32
- package/templates/web/src/components/theme-provider.tsx +0 -10
- package/templates/web/src/components/theme-toggle.tsx +0 -75
- package/templates/web/src/components/ui/alert-dialog.tsx +0 -157
- package/templates/web/src/components/ui/button.tsx +0 -58
- package/templates/web/src/components/ui/card.tsx +0 -92
- package/templates/web/src/components/ui/dropdown-menu.tsx +0 -200
- package/templates/web/src/components/ui/navigation-menu.tsx +0 -168
- package/templates/web/src/components/ui/toast.tsx +0 -128
- package/templates/web/src/components/ui/toaster.tsx +0 -35
- package/templates/web/src/components/ui/use-toast.ts +0 -187
- package/templates/web/src/hooks/useGeneratorMRU.ts +0 -57
- package/templates/web/src/lib/utils.ts +0 -6
- package/templates/web/tsconfig.json +0 -41
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
"""Supabase storage provider with integrated auth and CDN support."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import tempfile
|
|
5
|
-
from collections.abc import AsyncIterator
|
|
6
|
-
from datetime import UTC, datetime, timedelta
|
|
7
|
-
from typing import TYPE_CHECKING, Any
|
|
8
|
-
|
|
9
|
-
import aiofiles
|
|
10
|
-
|
|
11
|
-
if TYPE_CHECKING:
|
|
12
|
-
from supabase import AsyncClient, create_async_client
|
|
13
|
-
|
|
14
|
-
try:
|
|
15
|
-
from supabase import AsyncClient, create_async_client
|
|
16
|
-
|
|
17
|
-
_supabase_available = True
|
|
18
|
-
except ImportError:
|
|
19
|
-
# Handle case where supabase is not installed
|
|
20
|
-
create_async_client = None
|
|
21
|
-
# AsyncClient = None
|
|
22
|
-
_supabase_available = False
|
|
23
|
-
|
|
24
|
-
from ...logging import get_logger
|
|
25
|
-
from ..base import StorageException, StorageProvider
|
|
26
|
-
|
|
27
|
-
logger = get_logger(__name__)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class SupabaseStorageProvider(StorageProvider):
|
|
31
|
-
"""Supabase storage with integrated auth, CDN, and proper async patterns."""
|
|
32
|
-
|
|
33
|
-
def __init__(self, url: str, key: str, bucket: str):
|
|
34
|
-
if not _supabase_available:
|
|
35
|
-
raise ImportError("supabase-py is required for SupabaseStorageProvider")
|
|
36
|
-
|
|
37
|
-
self.url = url
|
|
38
|
-
self.key = key
|
|
39
|
-
self.bucket = bucket
|
|
40
|
-
self._client: AsyncClient | None = None
|
|
41
|
-
|
|
42
|
-
async def _get_client(self) -> "AsyncClient":
|
|
43
|
-
"""Get or create the async Supabase client."""
|
|
44
|
-
if self._client is None:
|
|
45
|
-
if create_async_client is None:
|
|
46
|
-
raise ImportError("Async Supabase client not available")
|
|
47
|
-
self._client = await create_async_client(self.url, self.key)
|
|
48
|
-
return self._client
|
|
49
|
-
|
|
50
|
-
async def upload(
|
|
51
|
-
self,
|
|
52
|
-
key: str,
|
|
53
|
-
content: bytes | AsyncIterator[bytes],
|
|
54
|
-
content_type: str,
|
|
55
|
-
metadata: dict[str, Any] | None = None,
|
|
56
|
-
) -> str:
|
|
57
|
-
try:
|
|
58
|
-
client = await self._get_client()
|
|
59
|
-
|
|
60
|
-
# Handle streaming content for large files
|
|
61
|
-
if isinstance(content, bytes):
|
|
62
|
-
file_content = content
|
|
63
|
-
else:
|
|
64
|
-
# Stream to temp file to avoid memory issues
|
|
65
|
-
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
|
|
66
|
-
tmp_file_path = tmp_file.name
|
|
67
|
-
|
|
68
|
-
# Use async file operations for streaming content
|
|
69
|
-
async with aiofiles.open(tmp_file_path, "wb") as f:
|
|
70
|
-
async for chunk in content:
|
|
71
|
-
await f.write(chunk)
|
|
72
|
-
|
|
73
|
-
# Read the temp file asynchronously and upload
|
|
74
|
-
async with aiofiles.open(tmp_file_path, "rb") as f:
|
|
75
|
-
file_content = await f.read()
|
|
76
|
-
|
|
77
|
-
# Clean up temp file
|
|
78
|
-
os.unlink(tmp_file_path)
|
|
79
|
-
|
|
80
|
-
# Use async Supabase client methods
|
|
81
|
-
response = await client.storage.from_(self.bucket).upload(
|
|
82
|
-
path=key,
|
|
83
|
-
file=file_content,
|
|
84
|
-
file_options={
|
|
85
|
-
"content-type": content_type,
|
|
86
|
-
"upsert": "false", # Prevent accidental overwrites
|
|
87
|
-
},
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
return response.path
|
|
91
|
-
|
|
92
|
-
except Exception as e:
|
|
93
|
-
if isinstance(e, StorageException):
|
|
94
|
-
raise
|
|
95
|
-
logger.error(f"Unexpected error uploading {key} to Supabase: {e}")
|
|
96
|
-
raise StorageException(f"Supabase upload failed: {e}") from e
|
|
97
|
-
|
|
98
|
-
async def download(self, key: str) -> bytes:
|
|
99
|
-
"""Download file content from Supabase storage."""
|
|
100
|
-
try:
|
|
101
|
-
client = await self._get_client()
|
|
102
|
-
response = await client.storage.from_(self.bucket).download(key)
|
|
103
|
-
|
|
104
|
-
return response
|
|
105
|
-
|
|
106
|
-
except Exception as e:
|
|
107
|
-
if isinstance(e, StorageException):
|
|
108
|
-
raise
|
|
109
|
-
logger.error(f"Failed to download {key} from Supabase: {e}")
|
|
110
|
-
raise StorageException(f"Download failed: {e}") from e
|
|
111
|
-
|
|
112
|
-
async def get_presigned_upload_url(
|
|
113
|
-
self,
|
|
114
|
-
key: str,
|
|
115
|
-
content_type: str,
|
|
116
|
-
expires_in: timedelta | None = None,
|
|
117
|
-
) -> dict[str, Any]:
|
|
118
|
-
"""Generate presigned URL for direct client uploads."""
|
|
119
|
-
if expires_in is None:
|
|
120
|
-
expires_in = timedelta(hours=1)
|
|
121
|
-
|
|
122
|
-
try:
|
|
123
|
-
client = await self._get_client()
|
|
124
|
-
response = await client.storage.from_(self.bucket).create_signed_upload_url(path=key)
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
"url": response["signed_url"],
|
|
128
|
-
"fields": {}, # Supabase doesn't use form fields like S3
|
|
129
|
-
"expires_at": (datetime.now(UTC) + expires_in).isoformat(),
|
|
130
|
-
}
|
|
131
|
-
except Exception as e:
|
|
132
|
-
if isinstance(e, StorageException):
|
|
133
|
-
raise
|
|
134
|
-
logger.error(f"Failed to create presigned upload URL for {key}: {e}")
|
|
135
|
-
raise StorageException(f"Presigned URL creation failed: {e}") from e
|
|
136
|
-
|
|
137
|
-
async def get_presigned_download_url(
|
|
138
|
-
self, key: str, expires_in: timedelta | None = None
|
|
139
|
-
) -> str:
|
|
140
|
-
"""Generate presigned URL for secure downloads."""
|
|
141
|
-
if expires_in is None:
|
|
142
|
-
expires_in = timedelta(hours=1)
|
|
143
|
-
|
|
144
|
-
try:
|
|
145
|
-
client = await self._get_client()
|
|
146
|
-
response = await client.storage.from_(self.bucket).create_signed_url(
|
|
147
|
-
path=key, expires_in=int(expires_in.total_seconds())
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
return response["signedURL"]
|
|
151
|
-
|
|
152
|
-
except Exception as e:
|
|
153
|
-
if isinstance(e, StorageException):
|
|
154
|
-
raise
|
|
155
|
-
logger.error(f"Failed to create presigned download URL for {key}: {e}")
|
|
156
|
-
raise StorageException(f"Presigned download URL creation failed: {e}") from e
|
|
157
|
-
|
|
158
|
-
async def delete(self, key: str) -> bool:
|
|
159
|
-
"""Delete file by storage key."""
|
|
160
|
-
try:
|
|
161
|
-
client = await self._get_client()
|
|
162
|
-
await client.storage.from_(self.bucket).remove([key]) # type: ignore[reportUnknownMemberType]
|
|
163
|
-
|
|
164
|
-
return True
|
|
165
|
-
|
|
166
|
-
except Exception as e:
|
|
167
|
-
logger.error(f"Unexpected error deleting {key} from Supabase: {e}")
|
|
168
|
-
raise StorageException(f"Delete failed: {e}") from e
|
|
169
|
-
|
|
170
|
-
async def exists(self, key: str) -> bool:
|
|
171
|
-
"""Check if file exists."""
|
|
172
|
-
try:
|
|
173
|
-
client = await self._get_client()
|
|
174
|
-
# Try to get file info - if it doesn't exist, this will error
|
|
175
|
-
await client.storage.from_(self.bucket).get_public_url(key)
|
|
176
|
-
# If we get here without error, the file exists
|
|
177
|
-
return True
|
|
178
|
-
except Exception:
|
|
179
|
-
# Any error means the file doesn't exist or we can't access it
|
|
180
|
-
return False
|
|
181
|
-
|
|
182
|
-
async def get_metadata(self, key: str) -> dict[str, Any]:
|
|
183
|
-
"""Get file metadata (size, modified date, etc.)."""
|
|
184
|
-
try:
|
|
185
|
-
client = await self._get_client()
|
|
186
|
-
# Supabase doesn't have a direct metadata endpoint
|
|
187
|
-
# We'll need to use the list method with a prefix
|
|
188
|
-
response = await client.storage.from_(self.bucket).list(
|
|
189
|
-
path="/".join(key.split("/")[:-1]) or "/"
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
# Find our file in the results
|
|
193
|
-
file_info = None
|
|
194
|
-
filename = key.split("/")[-1]
|
|
195
|
-
for item in response:
|
|
196
|
-
if item.get("name") == filename:
|
|
197
|
-
file_info = item
|
|
198
|
-
break
|
|
199
|
-
|
|
200
|
-
if not file_info:
|
|
201
|
-
raise StorageException(f"File not found: {key}")
|
|
202
|
-
|
|
203
|
-
metadata = file_info.get("metadata", {})
|
|
204
|
-
result = {
|
|
205
|
-
"size": file_info.get("size", 0),
|
|
206
|
-
"last_modified": file_info.get("updated_at"),
|
|
207
|
-
"content_type": file_info.get("mimetype"),
|
|
208
|
-
"etag": file_info.get("id"),
|
|
209
|
-
}
|
|
210
|
-
if isinstance(metadata, dict):
|
|
211
|
-
result.update(metadata)
|
|
212
|
-
return result
|
|
213
|
-
|
|
214
|
-
except Exception as e:
|
|
215
|
-
if isinstance(e, StorageException):
|
|
216
|
-
raise
|
|
217
|
-
logger.error(f"Failed to get metadata for {key} from Supabase: {e}")
|
|
218
|
-
raise StorageException(f"Get metadata failed: {e}") from e
|
|
@@ -1,446 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tenant isolation validation and enforcement utilities.
|
|
3
|
-
|
|
4
|
-
This module provides utilities to validate and enforce tenant isolation
|
|
5
|
-
across the application to ensure data security in multi-tenant deployments.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
from datetime import UTC
|
|
11
|
-
from typing import Any
|
|
12
|
-
from uuid import UUID
|
|
13
|
-
|
|
14
|
-
from sqlalchemy import select, text
|
|
15
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
16
|
-
|
|
17
|
-
from .config import settings
|
|
18
|
-
from .dbmodels import Boards, Generations, Users
|
|
19
|
-
from .logging import get_logger
|
|
20
|
-
|
|
21
|
-
logger = get_logger(__name__)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class TenantIsolationError(Exception):
|
|
25
|
-
"""Raised when tenant isolation validation fails."""
|
|
26
|
-
|
|
27
|
-
pass
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class TenantIsolationValidator:
|
|
31
|
-
"""
|
|
32
|
-
Utility class for validating tenant isolation in multi-tenant environments.
|
|
33
|
-
|
|
34
|
-
This class provides methods to:
|
|
35
|
-
1. Validate tenant-scoped queries
|
|
36
|
-
2. Check for cross-tenant data access
|
|
37
|
-
3. Ensure proper tenant filtering
|
|
38
|
-
4. Audit tenant isolation compliance
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
def __init__(self, db: AsyncSession):
|
|
42
|
-
self.db = db
|
|
43
|
-
|
|
44
|
-
async def validate_user_tenant_isolation(self, user_id: UUID, tenant_id: UUID) -> bool:
|
|
45
|
-
"""
|
|
46
|
-
Validate that a user belongs to the specified tenant.
|
|
47
|
-
|
|
48
|
-
Args:
|
|
49
|
-
user_id: UUID of the user
|
|
50
|
-
tenant_id: UUID of the tenant
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
True if user belongs to tenant, False otherwise
|
|
54
|
-
|
|
55
|
-
Raises:
|
|
56
|
-
TenantIsolationError: If validation fails
|
|
57
|
-
"""
|
|
58
|
-
try:
|
|
59
|
-
stmt = select(Users).where((Users.id == user_id) & (Users.tenant_id == tenant_id))
|
|
60
|
-
result = await self.db.execute(stmt)
|
|
61
|
-
user = result.scalar_one_or_none()
|
|
62
|
-
|
|
63
|
-
if not user:
|
|
64
|
-
logger.warning(
|
|
65
|
-
"User tenant isolation violation",
|
|
66
|
-
user_id=str(user_id),
|
|
67
|
-
expected_tenant=str(tenant_id),
|
|
68
|
-
)
|
|
69
|
-
return False
|
|
70
|
-
|
|
71
|
-
return True
|
|
72
|
-
|
|
73
|
-
except Exception as e:
|
|
74
|
-
logger.error(
|
|
75
|
-
"User tenant isolation validation failed",
|
|
76
|
-
user_id=str(user_id),
|
|
77
|
-
tenant_id=str(tenant_id),
|
|
78
|
-
error=str(e),
|
|
79
|
-
)
|
|
80
|
-
raise TenantIsolationError(f"User tenant validation failed: {e}") from e
|
|
81
|
-
|
|
82
|
-
async def validate_board_tenant_isolation(self, board_id: UUID, tenant_id: UUID) -> bool:
|
|
83
|
-
"""
|
|
84
|
-
Validate that a board belongs to the specified tenant.
|
|
85
|
-
|
|
86
|
-
Args:
|
|
87
|
-
board_id: UUID of the board
|
|
88
|
-
tenant_id: UUID of the tenant
|
|
89
|
-
|
|
90
|
-
Returns:
|
|
91
|
-
True if board belongs to tenant, False otherwise
|
|
92
|
-
"""
|
|
93
|
-
try:
|
|
94
|
-
stmt = select(Boards).where((Boards.id == board_id) & (Boards.tenant_id == tenant_id))
|
|
95
|
-
result = await self.db.execute(stmt)
|
|
96
|
-
board = result.scalar_one_or_none()
|
|
97
|
-
|
|
98
|
-
if not board:
|
|
99
|
-
logger.warning(
|
|
100
|
-
"Board tenant isolation violation",
|
|
101
|
-
board_id=str(board_id),
|
|
102
|
-
expected_tenant=str(tenant_id),
|
|
103
|
-
)
|
|
104
|
-
return False
|
|
105
|
-
|
|
106
|
-
return True
|
|
107
|
-
|
|
108
|
-
except Exception as e:
|
|
109
|
-
logger.error(
|
|
110
|
-
"Board tenant isolation validation failed",
|
|
111
|
-
board_id=str(board_id),
|
|
112
|
-
tenant_id=str(tenant_id),
|
|
113
|
-
error=str(e),
|
|
114
|
-
)
|
|
115
|
-
raise TenantIsolationError(f"Board tenant validation failed: {e}") from e
|
|
116
|
-
|
|
117
|
-
async def validate_generation_tenant_isolation(
|
|
118
|
-
self, generation_id: UUID, tenant_id: UUID
|
|
119
|
-
) -> bool:
|
|
120
|
-
"""
|
|
121
|
-
Validate that a generation belongs to the specified tenant.
|
|
122
|
-
"""
|
|
123
|
-
try:
|
|
124
|
-
stmt = select(Generations).where(
|
|
125
|
-
(Generations.id == generation_id) & (Generations.tenant_id == tenant_id)
|
|
126
|
-
)
|
|
127
|
-
result = await self.db.execute(stmt)
|
|
128
|
-
generation = result.scalar_one_or_none()
|
|
129
|
-
|
|
130
|
-
if not generation:
|
|
131
|
-
logger.warning(
|
|
132
|
-
"Generation tenant isolation violation",
|
|
133
|
-
generation_id=str(generation_id),
|
|
134
|
-
expected_tenant=str(tenant_id),
|
|
135
|
-
)
|
|
136
|
-
return False
|
|
137
|
-
|
|
138
|
-
return True
|
|
139
|
-
|
|
140
|
-
except Exception as e:
|
|
141
|
-
logger.error(
|
|
142
|
-
"Generation tenant isolation validation failed",
|
|
143
|
-
generation_id=str(generation_id),
|
|
144
|
-
tenant_id=str(tenant_id),
|
|
145
|
-
error=str(e),
|
|
146
|
-
)
|
|
147
|
-
raise TenantIsolationError(f"Generation tenant validation failed: {e}") from e
|
|
148
|
-
|
|
149
|
-
async def audit_tenant_isolation(self, tenant_id: UUID) -> dict[str, Any]:
|
|
150
|
-
"""
|
|
151
|
-
Perform comprehensive tenant isolation audit.
|
|
152
|
-
|
|
153
|
-
Args:
|
|
154
|
-
tenant_id: UUID of the tenant to audit
|
|
155
|
-
|
|
156
|
-
Returns:
|
|
157
|
-
Dictionary with audit results and statistics
|
|
158
|
-
"""
|
|
159
|
-
logger.info("Starting tenant isolation audit", tenant_id=str(tenant_id))
|
|
160
|
-
|
|
161
|
-
audit_results = {
|
|
162
|
-
"tenant_id": str(tenant_id),
|
|
163
|
-
"audit_timestamp": None,
|
|
164
|
-
"isolation_violations": [],
|
|
165
|
-
"statistics": {},
|
|
166
|
-
"recommendations": [],
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
try:
|
|
170
|
-
from datetime import datetime
|
|
171
|
-
|
|
172
|
-
audit_results["audit_timestamp"] = datetime.now(UTC).isoformat()
|
|
173
|
-
|
|
174
|
-
# 1. Check for orphaned records
|
|
175
|
-
orphaned_records = await self._check_orphaned_records(tenant_id)
|
|
176
|
-
if orphaned_records:
|
|
177
|
-
audit_results["isolation_violations"].extend(orphaned_records)
|
|
178
|
-
|
|
179
|
-
# 2. Check cross-tenant board memberships
|
|
180
|
-
cross_tenant_memberships = await self._check_cross_tenant_memberships(tenant_id)
|
|
181
|
-
if cross_tenant_memberships:
|
|
182
|
-
audit_results["isolation_violations"].extend(cross_tenant_memberships)
|
|
183
|
-
|
|
184
|
-
# 3. Gather tenant statistics
|
|
185
|
-
audit_results["statistics"] = await self._gather_tenant_statistics(tenant_id)
|
|
186
|
-
|
|
187
|
-
# 4. Generate recommendations
|
|
188
|
-
audit_results["recommendations"] = self._generate_isolation_recommendations(
|
|
189
|
-
audit_results["isolation_violations"]
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
logger.info(
|
|
193
|
-
"Tenant isolation audit completed",
|
|
194
|
-
tenant_id=str(tenant_id),
|
|
195
|
-
violations_count=len(audit_results["isolation_violations"]),
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
return audit_results
|
|
199
|
-
|
|
200
|
-
except Exception as e:
|
|
201
|
-
logger.error(
|
|
202
|
-
"Tenant isolation audit failed",
|
|
203
|
-
tenant_id=str(tenant_id),
|
|
204
|
-
error=str(e),
|
|
205
|
-
)
|
|
206
|
-
raise TenantIsolationError(f"Tenant isolation audit failed: {e}") from e
|
|
207
|
-
|
|
208
|
-
async def _check_orphaned_records(self, tenant_id: UUID) -> list[dict[str, Any]]:
|
|
209
|
-
"""Check for records that should belong to tenant but don't."""
|
|
210
|
-
violations = []
|
|
211
|
-
|
|
212
|
-
try:
|
|
213
|
-
# Check for users with boards in different tenants
|
|
214
|
-
stmt = text(
|
|
215
|
-
"""
|
|
216
|
-
SELECT u.id as user_id, b.id as board_id, b.tenant_id as board_tenant_id
|
|
217
|
-
FROM boards.users u
|
|
218
|
-
JOIN boards.boards b ON u.id = b.owner_id
|
|
219
|
-
WHERE u.tenant_id = :tenant_id AND b.tenant_id != :tenant_id
|
|
220
|
-
"""
|
|
221
|
-
)
|
|
222
|
-
result = await self.db.execute(stmt, {"tenant_id": tenant_id})
|
|
223
|
-
orphaned_boards = result.fetchall()
|
|
224
|
-
|
|
225
|
-
for row in orphaned_boards:
|
|
226
|
-
violations.append(
|
|
227
|
-
{
|
|
228
|
-
"type": "orphaned_board",
|
|
229
|
-
"description": f"User {row.user_id} owns board {row.board_id} in different tenant", # noqa: E501
|
|
230
|
-
"user_id": str(row.user_id),
|
|
231
|
-
"board_id": str(row.board_id),
|
|
232
|
-
"board_tenant_id": str(row.board_tenant_id),
|
|
233
|
-
}
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
# Check for generations with boards in different tenants
|
|
237
|
-
stmt = text(
|
|
238
|
-
"""
|
|
239
|
-
SELECT g.id as generation_id,
|
|
240
|
-
g.tenant_id,
|
|
241
|
-
g.board_id,
|
|
242
|
-
b.tenant_id as board_tenant_id
|
|
243
|
-
FROM boards.generations g
|
|
244
|
-
JOIN boards.boards b ON g.board_id = b.id
|
|
245
|
-
WHERE g.tenant_id = :tenant_id AND b.tenant_id != :tenant_id
|
|
246
|
-
"""
|
|
247
|
-
)
|
|
248
|
-
result = await self.db.execute(stmt, {"tenant_id": tenant_id})
|
|
249
|
-
orphaned_generations = result.fetchall()
|
|
250
|
-
|
|
251
|
-
for row in orphaned_generations:
|
|
252
|
-
violations.append(
|
|
253
|
-
{
|
|
254
|
-
"type": "orphaned_generation",
|
|
255
|
-
"description": f"Generation {row.generation_id} belongs to different tenant than its board", # noqa: E501
|
|
256
|
-
"generation_id": str(row.generation_id),
|
|
257
|
-
"board_id": str(row.board_id),
|
|
258
|
-
"board_tenant_id": str(row.board_tenant_id),
|
|
259
|
-
}
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
except Exception as e:
|
|
263
|
-
logger.error("Failed to check orphaned records", error=str(e))
|
|
264
|
-
|
|
265
|
-
return violations
|
|
266
|
-
|
|
267
|
-
async def _check_cross_tenant_memberships(self, tenant_id: UUID) -> list[dict[str, Any]]:
|
|
268
|
-
"""Check for cross-tenant board memberships."""
|
|
269
|
-
violations = []
|
|
270
|
-
|
|
271
|
-
try:
|
|
272
|
-
stmt = text(
|
|
273
|
-
"""
|
|
274
|
-
SELECT bm.board_id,
|
|
275
|
-
bm.user_id,
|
|
276
|
-
b.tenant_id as board_tenant_id,
|
|
277
|
-
u.tenant_id as user_tenant_id
|
|
278
|
-
FROM boards.board_members bm
|
|
279
|
-
JOIN boards.boards b ON bm.board_id = b.id
|
|
280
|
-
JOIN boards.users u ON bm.user_id = u.id
|
|
281
|
-
WHERE b.tenant_id = :tenant_id AND u.tenant_id != :tenant_id
|
|
282
|
-
"""
|
|
283
|
-
)
|
|
284
|
-
result = await self.db.execute(stmt, {"tenant_id": tenant_id})
|
|
285
|
-
cross_tenant_members = result.fetchall()
|
|
286
|
-
|
|
287
|
-
for row in cross_tenant_members:
|
|
288
|
-
violations.append(
|
|
289
|
-
{
|
|
290
|
-
"type": "cross_tenant_membership",
|
|
291
|
-
"description": "User from different tenant has board membership",
|
|
292
|
-
"board_id": str(row.board_id),
|
|
293
|
-
"user_id": str(row.user_id),
|
|
294
|
-
"board_tenant_id": str(row.board_tenant_id),
|
|
295
|
-
"user_tenant_id": str(row.user_tenant_id),
|
|
296
|
-
}
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
except Exception as e:
|
|
300
|
-
logger.error("Failed to check cross-tenant memberships", error=str(e))
|
|
301
|
-
|
|
302
|
-
return violations
|
|
303
|
-
|
|
304
|
-
async def _gather_tenant_statistics(self, tenant_id: UUID) -> dict[str, int]:
|
|
305
|
-
"""Gather statistics for the tenant."""
|
|
306
|
-
stats = {}
|
|
307
|
-
|
|
308
|
-
try:
|
|
309
|
-
# Count users
|
|
310
|
-
stmt = select(Users).where(Users.tenant_id == tenant_id)
|
|
311
|
-
result = await self.db.execute(stmt)
|
|
312
|
-
stats["users_count"] = len(result.scalars().all())
|
|
313
|
-
|
|
314
|
-
# Count boards
|
|
315
|
-
stmt = select(Boards).where(Boards.tenant_id == tenant_id)
|
|
316
|
-
result = await self.db.execute(stmt)
|
|
317
|
-
stats["boards_count"] = len(result.scalars().all())
|
|
318
|
-
|
|
319
|
-
# Count generations
|
|
320
|
-
stmt = select(Generations).where(Generations.tenant_id == tenant_id)
|
|
321
|
-
result = await self.db.execute(stmt)
|
|
322
|
-
stats["generations_count"] = len(result.scalars().all())
|
|
323
|
-
|
|
324
|
-
# Count board memberships
|
|
325
|
-
stmt = text(
|
|
326
|
-
"""
|
|
327
|
-
SELECT COUNT(*) as count
|
|
328
|
-
FROM boards.board_members bm
|
|
329
|
-
JOIN boards.boards b ON bm.board_id = b.id
|
|
330
|
-
WHERE b.tenant_id = :tenant_id
|
|
331
|
-
"""
|
|
332
|
-
)
|
|
333
|
-
result = await self.db.execute(stmt, {"tenant_id": tenant_id})
|
|
334
|
-
stats["board_memberships_count"] = result.scalar()
|
|
335
|
-
|
|
336
|
-
except Exception as e:
|
|
337
|
-
logger.error("Failed to gather tenant statistics", error=str(e))
|
|
338
|
-
|
|
339
|
-
return stats
|
|
340
|
-
|
|
341
|
-
def _generate_isolation_recommendations(self, violations: list[dict[str, Any]]) -> list[str]:
|
|
342
|
-
"""Generate recommendations based on isolation violations."""
|
|
343
|
-
recommendations = []
|
|
344
|
-
|
|
345
|
-
if not violations:
|
|
346
|
-
recommendations.append("Tenant isolation is properly maintained - no violations found")
|
|
347
|
-
return recommendations
|
|
348
|
-
|
|
349
|
-
violation_types = {v["type"] for v in violations}
|
|
350
|
-
|
|
351
|
-
if "orphaned_board" in violation_types:
|
|
352
|
-
recommendations.append(
|
|
353
|
-
"Fix orphaned boards by ensuring board tenant_id matches owner's tenant_id"
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
if "orphaned_generation" in violation_types:
|
|
357
|
-
recommendations.append(
|
|
358
|
-
"Fix orphaned generations by ensuring generation tenant_id matches board tenant_id"
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
if "cross_tenant_membership" in violation_types:
|
|
362
|
-
recommendations.append(
|
|
363
|
-
"Remove cross-tenant board memberships or migrate users to appropriate tenants"
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
recommendations.append("Run isolation audit regularly to detect future violations")
|
|
367
|
-
recommendations.append(
|
|
368
|
-
"Consider adding database constraints to prevent isolation violations"
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
return recommendations
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
async def ensure_tenant_isolation(
|
|
375
|
-
db: AsyncSession,
|
|
376
|
-
user_id: UUID | None,
|
|
377
|
-
tenant_id: UUID,
|
|
378
|
-
resource_type: str,
|
|
379
|
-
resource_id: UUID | None = None,
|
|
380
|
-
) -> None:
|
|
381
|
-
"""
|
|
382
|
-
Ensure tenant isolation for a specific operation.
|
|
383
|
-
|
|
384
|
-
Args:
|
|
385
|
-
db: Database session
|
|
386
|
-
user_id: ID of the user performing the operation
|
|
387
|
-
tenant_id: ID of the tenant context
|
|
388
|
-
resource_type: Type of resource being accessed (user, board, generation)
|
|
389
|
-
resource_id: ID of the specific resource (if applicable)
|
|
390
|
-
|
|
391
|
-
Raises:
|
|
392
|
-
TenantIsolationError: If isolation validation fails
|
|
393
|
-
"""
|
|
394
|
-
if not settings.multi_tenant_mode:
|
|
395
|
-
# Skip validation in single-tenant mode
|
|
396
|
-
return
|
|
397
|
-
|
|
398
|
-
validator = TenantIsolationValidator(db)
|
|
399
|
-
|
|
400
|
-
try:
|
|
401
|
-
# Validate user belongs to tenant
|
|
402
|
-
if user_id:
|
|
403
|
-
user_valid = await validator.validate_user_tenant_isolation(user_id, tenant_id)
|
|
404
|
-
if not user_valid:
|
|
405
|
-
raise TenantIsolationError(f"User {user_id} does not belong to tenant {tenant_id}")
|
|
406
|
-
|
|
407
|
-
# Validate resource belongs to tenant (if resource_id provided)
|
|
408
|
-
if resource_id:
|
|
409
|
-
if resource_type == "board":
|
|
410
|
-
board_valid = await validator.validate_board_tenant_isolation(
|
|
411
|
-
resource_id, tenant_id
|
|
412
|
-
)
|
|
413
|
-
if not board_valid:
|
|
414
|
-
raise TenantIsolationError(
|
|
415
|
-
f"Board {resource_id} does not belong to tenant {tenant_id}"
|
|
416
|
-
)
|
|
417
|
-
|
|
418
|
-
elif resource_type == "generation":
|
|
419
|
-
generation_valid = await validator.validate_generation_tenant_isolation(
|
|
420
|
-
resource_id, tenant_id
|
|
421
|
-
)
|
|
422
|
-
if not generation_valid:
|
|
423
|
-
raise TenantIsolationError(
|
|
424
|
-
f"Generation {resource_id} does not belong to tenant {tenant_id}"
|
|
425
|
-
)
|
|
426
|
-
|
|
427
|
-
logger.debug(
|
|
428
|
-
"Tenant isolation validated successfully",
|
|
429
|
-
user_id=str(user_id) if user_id else None,
|
|
430
|
-
tenant_id=str(tenant_id),
|
|
431
|
-
resource_type=resource_type,
|
|
432
|
-
resource_id=str(resource_id) if resource_id else None,
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
except TenantIsolationError:
|
|
436
|
-
# Re-raise isolation errors
|
|
437
|
-
raise
|
|
438
|
-
except Exception as e:
|
|
439
|
-
logger.error(
|
|
440
|
-
"Tenant isolation validation error",
|
|
441
|
-
user_id=str(user_id) if user_id else None,
|
|
442
|
-
tenant_id=str(tenant_id),
|
|
443
|
-
resource_type=resource_type,
|
|
444
|
-
error=str(e),
|
|
445
|
-
)
|
|
446
|
-
raise TenantIsolationError(f"Tenant isolation validation failed: {e}") from e
|