diff --git a/applications/jupyterhub/deploy/values.yaml b/applications/jupyterhub/deploy/values.yaml index 7117e526..9378f516 100755 --- a/applications/jupyterhub/deploy/values.yaml +++ b/applications/jupyterhub/deploy/values.yaml @@ -23,6 +23,7 @@ hub: c.JupyterHub.tornado_settings = { "headers": { "Content-Security-Policy": "frame-ancestors 'self' localhost *.osb.local *.opensourcebrain.org *.v2.opensourcebrain.org"}} spawner: >- c.Spawner.args = [] + namedServerLimitPerUser: 3 singleuser: storage: type: dynamic diff --git a/applications/osb-portal/src/components/workspace/WorkspaceEditor.tsx b/applications/osb-portal/src/components/workspace/WorkspaceEditor.tsx index e1e98836..91edcee9 100644 --- a/applications/osb-portal/src/components/workspace/WorkspaceEditor.tsx +++ b/applications/osb-portal/src/components/workspace/WorkspaceEditor.tsx @@ -176,6 +176,9 @@ export default (props: WorkspaceEditProps) => { }, (e) => { setLoading(false); + if (e.status === 405) { + throw new Error("Maximum number of workspaces exceeded."); + } throw new Error("Error submitting the workspace"); // console.error('Error submitting the workspace', e); } diff --git a/applications/workspaces/api/openapi.yaml b/applications/workspaces/api/openapi.yaml index 024c078b..670d3e63 100644 --- a/applications/workspaces/api/openapi.yaml +++ b/applications/workspaces/api/openapi.yaml @@ -126,6 +126,8 @@ paths: $ref: "#/components/schemas/Workspace" 400: description: The Workspace already exists. + 405: + description: Not allowed to create a new workspace /workspace/{id}: parameters: - in: path diff --git a/applications/workspaces/server/workspaces/config.py b/applications/workspaces/server/workspaces/config.py index e575f3a2..0b3accf1 100644 --- a/applications/workspaces/server/workspaces/config.py +++ b/applications/workspaces/server/workspaces/config.py @@ -28,6 +28,8 @@ class Config(object): APP_NAME = "workspaces" WSMGR_HOSTNAME = socket.gethostname() WSMGR_IPADDRESS = socket.gethostbyname(WSMGR_HOSTNAME) + # set the max number of workspaces per user + MAX_NUMBER_WORKSPACES_PER_USER = 3 try: CH_NAMESPACE = conf.get_configuration()["namespace"] diff --git a/applications/workspaces/server/workspaces/controllers/workspace_controller.py b/applications/workspaces/server/workspaces/controllers/workspace_controller.py index 4c9cc90a..50e6d674 100644 --- a/applications/workspaces/server/workspaces/controllers/workspace_controller.py +++ b/applications/workspaces/server/workspaces/controllers/workspace_controller.py @@ -8,7 +8,7 @@ from workspaces.repository.model_repository import WorkspaceImageRepository, WorkspaceRepository, db from workspaces.repository.models import WorkspaceEntity, WorkspaceImage from workspaces.helpers.etl_helpers import copy_origins -from workspaces.service.model_service import NotAuthorized, WorkspaceService +from workspaces.service.model_service import NotAuthorized, NotAllowed, WorkspaceService def _save_image(id_=None, image=None, filename_base=None): ext = mimetypes.guess_extension(image.mimetype) @@ -100,3 +100,5 @@ def workspace_clone(id_, body=None): return ws.to_dict() except NotAuthorized: return "Not authorized", 401 + except NotAllowed: + return "Not allowed", 405 diff --git a/applications/workspaces/server/workspaces/openapi/openapi.yaml b/applications/workspaces/server/workspaces/openapi/openapi.yaml index 48ed8c96..c34765fb 100644 --- a/applications/workspaces/server/workspaces/openapi/openapi.yaml +++ b/applications/workspaces/server/workspaces/openapi/openapi.yaml @@ -126,6 +126,8 @@ paths: $ref: '#/components/schemas/Workspace' "400": description: The Workspace already exists. + "405": + description: Not allowed to create a new workspace /workspace/{id}: parameters: - in: path diff --git a/applications/workspaces/server/workspaces/service/model_service.py b/applications/workspaces/server/workspaces/service/model_service.py index 5ebe6c0f..66bd3bdc 100644 --- a/applications/workspaces/server/workspaces/service/model_service.py +++ b/applications/workspaces/server/workspaces/service/model_service.py @@ -47,6 +47,10 @@ class NotAuthorized(Exception): pass +class NotAllowed(Exception): + pass + + class UserService(): def get(self, user_id): @@ -131,9 +135,6 @@ def is_authorized(self, object): raise NotImplementedError( f"Authorization not implemented for {self.__class__.__name__}") - - - class WorkspaceService(BaseModelService): repository = WorkspaceRepository() @@ -144,11 +145,24 @@ class WorkspaceService(BaseModelService): @staticmethod def get_pvc_name(workspace_id): return f"workspace-{workspace_id}" + + def check_max_num_workspaces_per_user(self, user_id=None): + if not user_id: + user_id = keycloak_user_id() + # check if max number of ws per user limit is reached + num_ws_current_user = self.repository.search(user_id=user_id).total + max_num_ws_current_user = Config.MAX_NUMBER_WORKSPACES_PER_USER + if num_ws_current_user >= max_num_ws_current_user: + raise NotAllowed( + f"Max number of {max_num_ws_current_user} workspaces " \ + "limit exceeded" + ) @send_event(message_type="workspace", operation="create") def post(self, body): if 'user_id' not in body: body['user_id'] = keycloak_user_id() + self.check_max_num_workspaces_per_user(body['user_id']) for r in body.get("resources", []): r.update({"origin": json.dumps(r.get("origin"))}) workspace = Workspace.from_dict(body) # Validate @@ -168,6 +182,8 @@ def get_workspace_volume_size(self, ws: Workspace): @send_event(message_type="workspace", operation="create") def clone(self, workspace_id): + user_id = keycloak_user_id() + self.check_max_num_workspaces_per_user(user_id) from workspaces.service.workflow import clone_workspaces_content workspace = self.get(workspace_id) if workspace is None: @@ -177,7 +193,7 @@ def clone(self, workspace_id): cloned = dict( name=f"Clone of {workspace['name']}", tags=workspace['tags'], - user_id=keycloak_user_id(), + user_id=user_id, description=workspace['description'], publicable=False, diff --git a/applications/workspaces/server/workspaces/views/api/rest_api_views.py b/applications/workspaces/server/workspaces/views/api/rest_api_views.py index 5fd4febc..d06b76db 100644 --- a/applications/workspaces/server/workspaces/views/api/rest_api_views.py +++ b/applications/workspaces/server/workspaces/views/api/rest_api_views.py @@ -2,6 +2,7 @@ from workspaces.service.model_service import ( NotAuthorized, + NotAllowed, OsbrepositoryService, VolumestorageService, WorkspaceService, @@ -15,6 +16,12 @@ class WorkspaceView(BaseModelView): service = WorkspaceService() + + def post(self, body): + try: + super().post(body) + except NotAllowed: + return "Not allowed", 405 class OsbrepositoryView(BaseModelView):