From ec3015f0bcfc163198bef3c2acbb7661ddac43f3 Mon Sep 17 00:00:00 2001 From: frcroth Date: Mon, 18 Nov 2024 09:55:44 +0100 Subject: [PATCH] Improve error messages for starting jobs on other orgs datasets (#8181) --- CHANGELOG.unreleased.md | 1 + app/controllers/AiModelController.scala | 13 ++++++++-- app/controllers/JobController.scala | 24 +++++++++++++------ conf/messages | 16 +++++++------ frontend/javascripts/admin/api/jobs.ts | 1 + .../view/action-bar/starting_job_modals.tsx | 1 + 6 files changed, 40 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 7cd632c60f..3784c9075c 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -14,6 +14,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released ### Changed - Reading image files on datastore filesystem is now done asynchronously. [#8126](https://github.com/scalableminds/webknossos/pull/8126) +- Improved error messages for starting jobs on datasets from other organizations. [#8181](https://github.com/scalableminds/webknossos/pull/8181) ### Fixed - Fix performance bottleneck when deleting a lot of trees at once. [#8176](https://github.com/scalableminds/webknossos/pull/8176) diff --git a/app/controllers/AiModelController.scala b/app/controllers/AiModelController.scala index e09d8a4f53..c8c2e9f204 100644 --- a/app/controllers/AiModelController.scala +++ b/app/controllers/AiModelController.scala @@ -1,5 +1,6 @@ package controllers +import com.scalableminds.util.accesscontext.GlobalAccessContext import com.scalableminds.util.geometry.{BoundingBox, Vec3Int} import com.scalableminds.util.tools.{Fox, FoxImplicits} import models.aimodels.{AiInference, AiInferenceDAO, AiInferenceService, AiModel, AiModelDAO, AiModelService} @@ -18,6 +19,7 @@ import scala.concurrent.ExecutionContext import com.scalableminds.util.time.Instant import models.aimodels.AiModelCategory.AiModelCategory import models.organization.OrganizationDAO +import play.api.i18n.Messages case class TrainingAnnotationSpecification(annotationId: ObjectId, colorLayerName: String, @@ -41,6 +43,7 @@ object RunTrainingParameters { case class RunInferenceParameters(annotationId: Option[ObjectId], aiModelId: ObjectId, datasetName: String, + organizationId: String, colorLayerName: String, boundingBox: String, newDatasetName: String, @@ -135,6 +138,7 @@ class AiModelController @Inject()( firstAnnotationId <- trainingAnnotations.headOption.map(_.annotationId).toFox annotation <- annotationDAO.findOne(firstAnnotationId) dataset <- datasetDAO.findOne(annotation._dataset) + _ <- bool2Fox(request.identity._organization == dataset._organization) ?~> "job.trainModel.notAllowed.organization" ~> FORBIDDEN dataStore <- dataStoreDAO.findOneByName(dataset._dataStore) ?~> "dataStore.notFound" _ <- Fox .serialCombined(request.body.trainingAnnotations.map(_.annotationId))(annotationDAO.findOne) ?~> "annotation.notFound" @@ -172,8 +176,13 @@ class AiModelController @Inject()( sil.SecuredAction.async(validateJson[RunInferenceParameters]) { implicit request => for { _ <- userService.assertIsSuperUser(request.identity) - organization <- organizationDAO.findOne(request.identity._organization) - dataset <- datasetDAO.findOneByNameAndOrganization(request.body.datasetName, organization._id) + organization <- organizationDAO.findOne(request.body.organizationId)(GlobalAccessContext) ?~> Messages( + "organization.notFound", + request.body.organizationId) + _ <- bool2Fox(request.identity._organization == organization._id) ?~> "job.runInference.notAllowed.organization" ~> FORBIDDEN + dataset <- datasetDAO.findOneByNameAndOrganization(request.body.datasetName, organization._id) ?~> Messages( + "dataset.notFound", + request.body.datasetName) dataStore <- dataStoreDAO.findOneByName(dataset._dataStore) ?~> "dataStore.notFound" _ <- aiModelDAO.findOne(request.body.aiModelId) ?~> "aiModel.notFound" _ <- datasetService.assertValidDatasetName(request.body.newDatasetName) diff --git a/app/controllers/JobController.scala b/app/controllers/JobController.scala index 8d45f63ed0..e82ecd543e 100644 --- a/app/controllers/JobController.scala +++ b/app/controllers/JobController.scala @@ -124,7 +124,7 @@ class JobController @Inject()( voxelSizeFactor <- Vec3Double.fromUriLiteral(scale).toFox voxelSizeUnit <- Fox.runOptional(unit)(u => LengthUnit.fromString(u).toFox) voxelSize = VoxelSize.fromFactorAndUnitWithDefault(voxelSizeFactor, voxelSizeUnit) - _ <- bool2Fox(request.identity._organization == organization._id) ~> FORBIDDEN + _ <- bool2Fox(request.identity._organization == organization._id) ?~> "job.convertToWkw.notAllowed.organization" ~> FORBIDDEN dataset <- datasetDAO.findOneByNameAndOrganization(datasetName, organization._id) ?~> Messages( "dataset.notFound", datasetName) ~> NOT_FOUND @@ -230,7 +230,9 @@ class JobController @Inject()( sil.SecuredAction.async { implicit request => log(Some(slackNotificationService.noticeFailedJobRequest)) { for { - organization <- organizationDAO.findOne(organizationId) ?~> Messages("organization.notFound", organizationId) + organization <- organizationDAO.findOne(organizationId)(GlobalAccessContext) ?~> Messages( + "organization.notFound", + organizationId) _ <- bool2Fox(request.identity._organization == organization._id) ?~> "job.inferNeurons.notAllowed.organization" ~> FORBIDDEN dataset <- datasetDAO.findOneByNameAndOrganization(datasetName, organization._id) ?~> Messages( "dataset.notFound", @@ -261,7 +263,9 @@ class JobController @Inject()( sil.SecuredAction.async { implicit request => log(Some(slackNotificationService.noticeFailedJobRequest)) { for { - organization <- organizationDAO.findOne(organizationId) ?~> Messages("organization.notFound", organizationId) + organization <- organizationDAO.findOne(organizationId)(GlobalAccessContext) ?~> Messages( + "organization.notFound", + organizationId) _ <- bool2Fox(request.identity._organization == organization._id) ?~> "job.inferMitochondria.notAllowed.organization" ~> FORBIDDEN dataset <- datasetDAO.findOneByNameAndOrganization(datasetName, organization._id) ?~> Messages( "dataset.notFound", @@ -293,7 +297,9 @@ class JobController @Inject()( sil.SecuredAction.async { implicit request => log(Some(slackNotificationService.noticeFailedJobRequest)) { for { - organization <- organizationDAO.findOne(organizationId) ?~> Messages("organization.notFound", organizationId) + organization <- organizationDAO.findOne(organizationId)(GlobalAccessContext) ?~> Messages( + "organization.notFound", + organizationId) _ <- bool2Fox(request.identity._organization == organization._id) ?~> "job.alignSections.notAllowed.organization" ~> FORBIDDEN dataset <- datasetDAO.findOneByNameAndOrganization(datasetName, organization._id) ?~> Messages( "dataset.notFound", @@ -412,7 +418,9 @@ class JobController @Inject()( sil.SecuredAction.async { implicit request => log(Some(slackNotificationService.noticeFailedJobRequest)) { for { - organization <- organizationDAO.findOne(organizationId) ?~> Messages("organization.notFound", organizationId) + organization <- organizationDAO.findOne(organizationId)(GlobalAccessContext) ?~> Messages( + "organization.notFound", + organizationId) _ <- bool2Fox(request.identity._organization == organization._id) ?~> "job.findLargestSegmentId.notAllowed.organization" ~> FORBIDDEN dataset <- datasetDAO.findOneByNameAndOrganization(datasetName, organization._id) ?~> Messages( "dataset.notFound", @@ -434,9 +442,11 @@ class JobController @Inject()( sil.SecuredAction.async(validateJson[AnimationJobOptions]) { implicit request => log(Some(slackNotificationService.noticeFailedJobRequest)) { for { - organization <- organizationDAO.findOne(organizationId) ?~> Messages("organization.notFound", organizationId) - userOrganization <- organizationDAO.findOne(request.identity._organization) + organization <- organizationDAO.findOne(organizationId)(GlobalAccessContext) ?~> Messages( + "organization.notFound", + organizationId) _ <- bool2Fox(request.identity._organization == organization._id) ?~> "job.renderAnimation.notAllowed.organization" ~> FORBIDDEN + userOrganization <- organizationDAO.findOne(request.identity._organization) dataset <- datasetDAO.findOneByNameAndOrganization(datasetName, organization._id) ?~> Messages( "dataset.notFound", datasetName) ~> NOT_FOUND diff --git a/conf/messages b/conf/messages index 4f59c9f0e8..34aa12fa2a 100644 --- a/conf/messages +++ b/conf/messages @@ -138,7 +138,7 @@ dataLayer.wrongMag=DataLayer “{0}” does not have mag “{1}” dataLayer.invalidMag=Supplied “{0}” is not a valid mag format. Please use “x-y-z” zarr.invalidChunkCoordinates=Invalid chunk coordinates. Expected dot separated coordinates like c.x.y.z -zarr.invalidFirstChunkCoord="First Channel must be 0" +zarr.invalidFirstChunkCoord=First Channel must be 0 zarr.chunkNotFound=Could not find the requested chunk zarr.notEnoughCoordinates=Invalid number of chunk coordinates. Expected to get at least 3 dimensions and channel 0. zarr.invalidAdditionalCoordinates=Invalid additional coordinates for this data layer. @@ -150,8 +150,8 @@ nml.file.noFile=No file or file with empty content uploaded nml.file.differentDatasets=Cannot upload tracings that belong to different datasets at once nml.parameters.notFound=No parameters section found nml.element.invalid=Invalid {0} -nml.comment.node.invalid=Invalid node id of comment {0}" -nml.node.id.invalid=Invalid {0} node id {1}" +nml.comment.node.invalid=Invalid node id of comment {0} +nml.node.id.invalid=Invalid {0} node id {1} nml.node.attribute.invalid=Invalid node {0} for node id {1} nml.edge.invalid=Invalid edge {0} {1} nml.tree.id.invalid=Invalid tree id {0} @@ -321,6 +321,7 @@ job.invalidBoundingBox = The selected bounding box could not be parsed, must be job.invalidMag = The selected mag could not be parsed, must be x-y-z job.volumeExceeded = The volume of the selected bounding box is too large. job.edgeLengthExceeded = An edge length of the selected bounding box is too large. +job.convertToWkw.notAllowed.organization = Converting to WKW is only allowed for datasets of your own organization. job.inferNuclei.notAllowed.organization = Currently nuclei inferral is only allowed for datasets of your own organization. job.inferNeurons.notAllowed.organization = Currently neuron inferral is only allowed for datasets of your own organization. job.meshFile.notAllowed.organization = Calculating mesh files is only allowed for datasets of your own organization. @@ -329,10 +330,11 @@ job.globalizeFloodfill.notAllowed.organization = Globalizing floodfills is only job.materializeVolumeAnnotation.notAllowed.organization = Materializing volume annotations is only allowed for datasets of your own organization. job.noWorkerForDatastoreAndJob = No webknossos-worker supporting the requested job is available for the selected datastore. job.emailNotifactionsDisabled = Email notifications are not enabled for this job type. -job.renderAnimation.notAllowed.organization = "Rendering animations is only allowed for datasets of your own organization." -job.alignSections.notAllowed.organization = "Aligning sections is only allowed for datasets of your own organization." -job.alignSections.notAllowed.onlySuperUsers = "For now, aligning sections is only allowed for super users." -job.additionalCoordinates.invalid = "The passed additional coordinates are invalid." +job.renderAnimation.notAllowed.organization = Rendering animations is only allowed for datasets of your own organization. +job.alignSections.notAllowed.organization = Aligning sections is only allowed for datasets of your own organization. +job.additionalCoordinates.invalid = The passed additional coordinates are invalid. +job.trainModel.notAllowed.organization = Training AI models is only allowed for datasets of your own organization. +job.runInference.notAllowed.organization = Running inference is only allowed for datasets of your own organization. voxelytics.disabled = Voxelytics workflow reporting and logging are not enabled for this WEBKNOSSOS instance. voxelytics.runNotFound = Workflow runs not found diff --git a/frontend/javascripts/admin/api/jobs.ts b/frontend/javascripts/admin/api/jobs.ts index 99d1c6e1bf..796ee43352 100644 --- a/frontend/javascripts/admin/api/jobs.ts +++ b/frontend/javascripts/admin/api/jobs.ts @@ -348,6 +348,7 @@ type RunInferenceParameters = { annotationId?: string; aiModelId: string; datasetName: string; + organizationId: string; colorLayerName: string; boundingBox: Vector6; newDatasetName: string; diff --git a/frontend/javascripts/oxalis/view/action-bar/starting_job_modals.tsx b/frontend/javascripts/oxalis/view/action-bar/starting_job_modals.tsx index a7b75bad1f..a3f071a2fb 100644 --- a/frontend/javascripts/oxalis/view/action-bar/starting_job_modals.tsx +++ b/frontend/javascripts/oxalis/view/action-bar/starting_job_modals.tsx @@ -829,6 +829,7 @@ function CustomAiModelInferenceForm() { aiModelId: form.getFieldValue("aiModel"), workflowYaml: useCustomWorkflow ? form.getFieldValue("workflowYaml") : undefined, datasetName: dataset.name, + organizationId: dataset.owningOrganization, colorLayerName: colorLayer.name, boundingBox, newDatasetName: newDatasetName,