diff --git a/app/stardag-api/src/stardag_api/limits.py b/app/stardag-api/src/stardag_api/limits.py index 45d7711b..c0d9572c 100644 --- a/app/stardag-api/src/stardag_api/limits.py +++ b/app/stardag-api/src/stardag_api/limits.py @@ -40,6 +40,10 @@ class LimitsSettings(BaseSettings): max_dependency_ids_per_task: Annotated[int, Field(ge=1)] | None = None max_artifacts_per_task: Annotated[int, Field(ge=1)] | None = None + # Tenancy quotas (enforced in routes/workspaces.py) + max_workspaces_per_user: Annotated[int, Field(ge=1)] | None = None + max_environments_per_workspace: Annotated[int, Field(ge=1)] | None = None + # Cache TTL for DB count queries (seconds) entity_count_cache_ttl: Annotated[int, Field(ge=1)] = 60 diff --git a/app/stardag-api/src/stardag_api/models/workspace.py b/app/stardag-api/src/stardag_api/models/workspace.py index ddebd483..e54f36e7 100644 --- a/app/stardag-api/src/stardag_api/models/workspace.py +++ b/app/stardag-api/src/stardag_api/models/workspace.py @@ -25,10 +25,6 @@ class Workspace(Base, TimestampMixin): __tablename__ = "workspaces" - # Limits - MAX_WORKSPACES_PER_USER = 3 - MAX_ENVIRONMENTS_PER_WORKSPACE = 6 - id: Mapped[UUID] = mapped_column( Uuid, primary_key=True, diff --git a/app/stardag-api/src/stardag_api/routes/workspaces.py b/app/stardag-api/src/stardag_api/routes/workspaces.py index 0a3c2ed0..2ef910e5 100644 --- a/app/stardag-api/src/stardag_api/routes/workspaces.py +++ b/app/stardag-api/src/stardag_api/routes/workspaces.py @@ -17,6 +17,7 @@ from sqlalchemy.orm import selectinload from stardag_api.auth import get_current_user, get_current_user_flexible +from stardag_api.config import limits_settings from stardag_api.db import get_db from stardag_api.models import ( Invite, @@ -231,10 +232,11 @@ async def create_workspace( ) ) workspace_count = workspace_count_result.scalar() or 0 - if workspace_count >= Workspace.MAX_WORKSPACES_PER_USER: + max_workspaces = limits_settings.max_workspaces_per_user + if max_workspaces is not None and workspace_count >= max_workspaces: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail=f"You can create at most {Workspace.MAX_WORKSPACES_PER_USER} workspaces", + detail=f"You can create at most {max_workspaces} workspaces", ) # Check if slug is taken @@ -760,10 +762,11 @@ async def create_environment( ) ) environment_count = environment_count_result.scalar() or 0 - if environment_count >= Workspace.MAX_ENVIRONMENTS_PER_WORKSPACE: + max_environments = limits_settings.max_environments_per_workspace + if max_environments is not None and environment_count >= max_environments: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail=f"Workspace can have at most {Workspace.MAX_ENVIRONMENTS_PER_WORKSPACE} environments", + detail=f"Workspace can have at most {max_environments} environments", ) # Check if slug exists in this workspace diff --git a/infra/aws-cdk/lib/api-stack.ts b/infra/aws-cdk/lib/api-stack.ts index 6c52bb83..f5ca3772 100644 --- a/infra/aws-cdk/lib/api-stack.ts +++ b/infra/aws-cdk/lib/api-stack.ts @@ -209,6 +209,8 @@ export class ApiStack extends cdk.Stack { LIMITS_MAX_ASSETS_PER_WORKSPACE_24H: "1000", LIMITS_MAX_DEPENDENCY_IDS_PER_TASK: "500", LIMITS_MAX_ASSETS_PER_TASK: "10", + LIMITS_MAX_WORKSPACES_PER_USER: "3", + LIMITS_MAX_ENVIRONMENTS_PER_WORKSPACE: "15", // Worker count is read by the Dockerfile CMD; keep undefined here // to fall through to the image's default (sized for 1 vCPU). ...(apiGunicornWorkers ? { GUNICORN_WORKERS: apiGunicornWorkers } : {}),