Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add switch orga to legacy routes #8257

Merged
merged 17 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
### Fixed
- Fixed that listing datasets with the `api/datasets` route without compression failed due to missing permissions regarding public datasets. [#8249](https://github.com/scalableminds/webknossos/pull/8249)
- Fixed a bug that uploading a zarr dataset with an already existing `datasource-properties.json` file failed. [#8268](https://github.com/scalableminds/webknossos/pull/8268)
- Fixed the organization switching feature for datasets opened via old links. [#8257](https://github.com/scalableminds/webknossos/pull/8257)
- Fixed that the frontend did not ensure a minium length for annotation layer names. Moreover, names starting with a `.` are also disallowed now. [#8244](https://github.com/scalableminds/webknossos/pull/8244)
- Fixed a bug where in the add remote dataset view the dataset name setting was not in sync with the datasource setting of the advanced tab making the form not submittable. [#8245](https://github.com/scalableminds/webknossos/pull/8245)
- Fix read and update dataset route for versions 8 and lower. [#8263](https://github.com/scalableminds/webknossos/pull/8263)
Expand Down
128 changes: 15 additions & 113 deletions app/controllers/AuthenticationController.scala
Original file line number Diff line number Diff line change
@@ -1,41 +1,29 @@
package controllers

import org.apache.pekko.actor.ActorSystem
import play.silhouette.api.actions.SecuredRequest
import play.silhouette.api.exceptions.ProviderException
import play.silhouette.api.services.AuthenticatorResult
import play.silhouette.api.util.{Credentials, PasswordInfo}
import play.silhouette.api.{LoginInfo, Silhouette}
import play.silhouette.impl.providers.CredentialsProvider
import com.scalableminds.util.accesscontext.{AuthorizedAccessContext, DBAccessContext, GlobalAccessContext}
import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext}
import com.scalableminds.util.objectid.ObjectId
import com.scalableminds.util.tools.{Fox, FoxImplicits, TextUtils}
import mail.{DefaultMails, MailchimpClient, MailchimpTag, Send}
import models.analytics.{AnalyticsService, InviteEvent, JoinOrganizationEvent, SignupEvent}
import models.annotation.AnnotationState.Cancelled
import models.annotation.{AnnotationDAO, AnnotationIdentifier, AnnotationInformationProvider}
import models.dataset.DatasetDAO
import models.organization.{Organization, OrganizationDAO, OrganizationService}
import models.user._
import models.voxelytics.VoxelyticsDAO
import net.liftweb.common.{Box, Empty, Failure, Full}
import org.apache.commons.codec.binary.Base64
import org.apache.commons.codec.digest.{HmacAlgorithms, HmacUtils}
import org.apache.pekko.actor.ActorSystem
import play.api.data.Form
import play.api.data.Forms._
import play.api.data.validation.Constraints._
import play.api.i18n.{Messages, MessagesProvider}
import play.api.libs.json._
import play.api.mvc.{Action, AnyContent, Cookie, PlayBodyParsers, Request, Result}
import security.{
CombinedAuthenticator,
OpenIdConnectClient,
OpenIdConnectUserInfo,
PasswordHasher,
TokenType,
WkEnv,
WkSilhouetteEnvironment
}
import play.api.mvc._
import play.silhouette.api.actions.SecuredRequest
import play.silhouette.api.exceptions.ProviderException
import play.silhouette.api.services.AuthenticatorResult
import play.silhouette.api.util.{Credentials, PasswordInfo}
import play.silhouette.api.{LoginInfo, Silhouette}
import play.silhouette.impl.providers.CredentialsProvider
import security._
import utils.WkConf

import java.net.URLEncoder
Expand All @@ -49,20 +37,17 @@ class AuthenticationController @Inject()(
credentialsProvider: CredentialsProvider,
passwordHasher: PasswordHasher,
userService: UserService,
annotationProvider: AnnotationInformationProvider,
authenticationService: AccessibleBySwitchingService,
organizationService: OrganizationService,
inviteService: InviteService,
inviteDAO: InviteDAO,
mailchimpClient: MailchimpClient,
organizationDAO: OrganizationDAO,
analyticsService: AnalyticsService,
userDAO: UserDAO,
datasetDAO: DatasetDAO,
multiUserDAO: MultiUserDAO,
defaultMails: DefaultMails,
conf: WkConf,
annotationDAO: AnnotationDAO,
voxelyticsDAO: VoxelyticsDAO,
wkSilhouetteEnvironment: WkSilhouetteEnvironment,
openIdConnectClient: OpenIdConnectClient,
initialDataService: InitialDataService,
Expand Down Expand Up @@ -228,103 +213,20 @@ class AuthenticationController @Inject()(
result <- combinedAuthenticatorService.embed(cookie, Redirect("/dashboard")) //to login the new user
} yield result

/*
superadmin - can definitely switch, find organization via global access context
not superadmin - fetch all identities, construct access context, try until one works
*/

def accessibleBySwitching(datasetId: Option[String],
annotationId: Option[String],
workflowHash: Option[String]): Action[AnyContent] = sil.SecuredAction.async {
implicit request =>
for {
datasetIdValidated <- Fox.runOptional(datasetId)(ObjectId.fromString(_))
isSuperUser <- multiUserDAO.findOne(request.identity._multiUser).map(_.isSuperUser)
selectedOrganization <- if (isSuperUser)
accessibleBySwitchingForSuperUser(datasetIdValidated, annotationId, workflowHash)
else
accessibleBySwitchingForMultiUser(request.identity._multiUser, datasetIdValidated, annotationId, workflowHash)
_ <- bool2Fox(selectedOrganization._id != request.identity._organization) // User is already in correct orga, but still could not see dataset. Assume this had a reason.
selectedOrganization <- authenticationService.getOrganizationToSwitchTo(request.identity,
datasetIdValidated,
annotationId,
workflowHash)
selectedOrganizationJs <- organizationService.publicWrites(selectedOrganization)
} yield Ok(selectedOrganizationJs)
}

private def accessibleBySwitchingForSuperUser(datasetIdOpt: Option[ObjectId],
annotationIdOpt: Option[String],
workflowHashOpt: Option[String]): Fox[Organization] = {
implicit val ctx: DBAccessContext = GlobalAccessContext
(datasetIdOpt, annotationIdOpt, workflowHashOpt) match {
case (Some(datasetId), None, None) =>
for {
dataset <- datasetDAO.findOne(datasetId)
organization <- organizationDAO.findOne(dataset._organization)
} yield organization
case (None, Some(annotationId), None) =>
for {
annotationObjectId <- ObjectId.fromString(annotationId)
annotation <- annotationDAO.findOne(annotationObjectId) // Note: this does not work for compound annotations.
user <- userDAO.findOne(annotation._user)
organization <- organizationDAO.findOne(user._organization)
} yield organization
case (None, None, Some(workflowHash)) =>
for {
workflow <- voxelyticsDAO.findWorkflowByHash(workflowHash)
organization <- organizationDAO.findOne(workflow._organization)
} yield organization
case _ => Fox.failure("Can either test access for dataset or annotation or workflow, not a combination")
}
}

private def accessibleBySwitchingForMultiUser(multiUserId: ObjectId,
datasetIdOpt: Option[ObjectId],
annotationIdOpt: Option[String],
workflowHashOpt: Option[String]): Fox[Organization] =
for {
identities <- userDAO.findAllByMultiUser(multiUserId)
selectedIdentity <- Fox.find(identities)(identity =>
canAccessDatasetOrAnnotationOrWorkflow(identity, datasetIdOpt, annotationIdOpt, workflowHashOpt))
selectedOrganization <- organizationDAO.findOne(selectedIdentity._organization)(GlobalAccessContext)
} yield selectedOrganization

private def canAccessDatasetOrAnnotationOrWorkflow(user: User,
datasetIdOpt: Option[ObjectId],
annotationIdOpt: Option[String],
workflowHashOpt: Option[String]): Fox[Boolean] = {
val ctx = AuthorizedAccessContext(user)
(datasetIdOpt, annotationIdOpt, workflowHashOpt) match {
case (Some(datasetId), None, None) =>
canAccessDataset(ctx, datasetId)
case (None, Some(annotationId), None) =>
canAccessAnnotation(user, ctx, annotationId)
case (None, None, Some(workflowHash)) =>
canAccessWorkflow(user, workflowHash)
case _ => Fox.failure("Can either test access for dataset or annotation or workflow, not a combination")
}
}

private def canAccessDataset(ctx: DBAccessContext, datasetId: ObjectId): Fox[Boolean] = {
val foundFox = datasetDAO.findOne(datasetId)(ctx)
foundFox.futureBox.map(_.isDefined)
}

private def canAccessAnnotation(user: User, ctx: DBAccessContext, annotationId: String): Fox[Boolean] = {
val foundFox = for {
annotationIdParsed <- ObjectId.fromString(annotationId)
annotation <- annotationDAO.findOne(annotationIdParsed)(GlobalAccessContext)
_ <- bool2Fox(annotation.state != Cancelled)
restrictions <- annotationProvider.restrictionsFor(AnnotationIdentifier(annotation.typ, annotationIdParsed))(ctx)
_ <- restrictions.allowAccess(user)
} yield ()
foundFox.futureBox.map(_.isDefined)
}

private def canAccessWorkflow(user: User, workflowHash: String): Fox[Boolean] = {
val foundFox = for {
_ <- voxelyticsDAO.findWorkflowByHashAndOrganization(user._organization, workflowHash)
} yield ()
foundFox.futureBox.map(_.isDefined)
}

def joinOrganization(inviteToken: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
for {
invite <- inviteDAO.findOneByTokenValue(inviteToken) ?~> "invite.invalidToken"
Expand Down
39 changes: 28 additions & 11 deletions app/controllers/DatasetController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import models.folder.FolderService
import models.organization.OrganizationDAO
import models.team.{TeamDAO, TeamService}
import models.user.{User, UserDAO, UserService}
import net.liftweb.common.{Failure, Full}
import net.liftweb.common.{Empty, Failure, Full}
import play.api.i18n.{Messages, MessagesProvider}
import play.api.libs.functional.syntax._
import play.api.libs.json._
import play.api.mvc.{Action, AnyContent, PlayBodyParsers}
import play.silhouette.api.Silhouette
import security.{URLSharing, WkEnv}
import security.{AccessibleBySwitchingService, URLSharing, WkEnv}
import utils.{MetadataAssertions, WkConf}

import javax.inject.Inject
Expand Down Expand Up @@ -85,6 +85,7 @@ class DatasetController @Inject()(userService: UserService,
thumbnailService: ThumbnailService,
thumbnailCachingService: ThumbnailCachingService,
conf: WkConf,
authenticationService: AccessibleBySwitchingService,
analyticsService: AnalyticsService,
mailchimpClient: MailchimpClient,
wkExploreRemoteLayerService: WKExploreRemoteLayerService,
Expand Down Expand Up @@ -317,7 +318,7 @@ class DatasetController @Inject()(userService: UserService,
def update(datasetId: String): Action[JsValue] =
sil.SecuredAction.async(parse.json) { implicit request =>
withJsonBodyUsing(datasetPublicReads) {
case (description, datasetName, legacyDatasetDisplayName, sortingKey, isPublic, tags, metadata, folderId) => {
case (description, datasetName, legacyDatasetDisplayName, sortingKey, isPublic, tags, metadata, folderId) =>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My IDE complained about redundant brackets. Thus, I removed them

val name = if (legacyDatasetDisplayName.isDefined) legacyDatasetDisplayName else datasetName
for {
datasetIdValidated <- ObjectId.fromString(datasetId)
Expand All @@ -339,7 +340,6 @@ class DatasetController @Inject()(userService: UserService,
_ = analyticsService.track(ChangeDatasetSettingsEvent(request.identity, updated))
js <- datasetService.publicWrites(updated, Some(request.identity))
} yield Ok(Json.toJson(js))
}
}
}

Expand Down Expand Up @@ -406,13 +406,30 @@ class DatasetController @Inject()(userService: UserService,
def getDatasetIdFromNameAndOrganization(datasetName: String, organizationId: String): Action[AnyContent] =
sil.UserAwareAction.async { implicit request =>
for {
dataset <- datasetDAO.findOneByNameAndOrganization(datasetName, organizationId) ?~> notFoundMessage(datasetName) ~> NOT_FOUND
} yield
Ok(
Json.obj("id" -> dataset._id,
"name" -> dataset.name,
"organization" -> dataset._organization,
"directoryName" -> dataset.directoryName))
datasetBox <- datasetDAO.findOneByNameAndOrganization(datasetName, organizationId).futureBox
result <- (datasetBox match {
case Full(dataset) =>
Fox.successful(
Ok(
Json.obj("id" -> dataset._id,
"name" -> dataset.name,
"organization" -> dataset._organization,
"directoryName" -> dataset.directoryName)))
case Empty =>
for {
user <- request.identity.toFox ~> Unauthorized
dataset <- datasetDAO.findOneByNameAndOrganization(datasetName, organizationId)(GlobalAccessContext)
// Just checking if the user can switch to an organization to access the dataset.
_ <- authenticationService.getOrganizationToSwitchTo(user, Some(dataset._id), None, None)
} yield
Ok(
Json.obj("id" -> dataset._id,
"name" -> dataset.name,
"organization" -> dataset._organization,
"directoryName" -> dataset.directoryName))
case _ => Fox.failure(notFoundMessage(datasetName))
}) ?~> notFoundMessage(datasetName) ~> NOT_FOUND
} yield result
}

private def notFoundMessage(datasetName: String)(implicit ctx: DBAccessContext, m: MessagesProvider): String =
Expand Down
9 changes: 4 additions & 5 deletions app/models/dataset/Dataset.scala
Original file line number Diff line number Diff line change
Expand Up @@ -430,16 +430,15 @@ class DatasetDAO @Inject()(sqlClient: SqlClient, datasetLayerDAO: DatasetLayerDA
exists <- r.headOption
} yield exists

// Datasets are looked up by name and directoryName, as datasets from before dataset renaming was possible
// should have their directory name equal to their name during the time the link was created. This heuristic should
// have the best expected outcome as it expect to find the dataset by directoryName and it to be the oldest. In case
// someone renamed a dataset and created the link with a tool that uses the outdated dataset identification, the dataset should still be found.
// Legacy links to Datasets used their name and organizationId as identifier. In #8075 name was changed to directoryName.
// Thus, interpreting the name as the directory name should work, as changing the directory name is not possible.
// This way of looking up datasets should only be used for backwards compatibility.
def findOneByNameAndOrganization(name: String, organizationId: String)(implicit ctx: DBAccessContext): Fox[Dataset] =
for {
accessQuery <- readAccessQuery
r <- run(q"""SELECT $columns
FROM $existingCollectionName
WHERE (directoryName = $name OR name = $name)
WHERE (directoryName = $name)
AND _organization = $organizationId
AND $accessQuery
ORDER BY created ASC
Expand Down
Loading