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

Store remote dataset credentials separately #6646

Merged
merged 41 commits into from
Jan 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
17b1e1d
Add schema for credentials
frcroth Nov 15, 2022
d9a2fc0
Add credential model
frcroth Nov 17, 2022
1f71991
Simplify AnyCredential
frcroth Nov 17, 2022
c7dedd9
Rename httpbasicauth
frcroth Nov 18, 2022
3ed890c
Add routes for credential creation
frcroth Nov 18, 2022
07a6f28
Fix compilation
frcroth Nov 18, 2022
3783227
Add internal route to get credential
frcroth Nov 18, 2022
b91dfb3
Fix compilation error
frcroth Nov 22, 2022
7dab935
Fix things
frcroth Nov 22, 2022
149d3ea
[wip] pass injected fileSystemService to BucketProvider
fm3 Nov 27, 2022
9308dcc
Merge branch 'master' into store-credentials-separately
fm3 Nov 27, 2022
180816a
propagate execution context to bucket loading, change signature to fox
fm3 Nov 27, 2022
a3543c8
Fetch credentials from db for maglocator
frcroth Nov 28, 2022
359110a
Remove remote source from maglocator
frcroth Nov 28, 2022
5f15b6b
Make scope optional
frcroth Dec 2, 2022
034b670
Merge, bump schema Version to 93
frcroth Dec 2, 2022
a02fb34
Remove unused import
frcroth Dec 2, 2022
09875db
Merge branch 'master' into store-credentials-separately
frcroth Dec 8, 2022
63ad74b
Return credential id on credential creation
frcroth Dec 8, 2022
5779924
Make request silent
frcroth Dec 13, 2022
91f82b3
Merge branch 'master' into store-credentials-separately
frcroth Dec 13, 2022
f3cee82
add bucket provider cache
fm3 Dec 13, 2022
4a55885
format
fm3 Dec 13, 2022
ec77a8e
Allow credentials to be created when exploring
frcroth Dec 19, 2022
aab1d33
Update schema to include user, org, isDeleted
frcroth Dec 19, 2022
8434a9e
Change backend format command
frcroth Dec 13, 2022
8ceabb2
Merge branch 'master' into store-credentials-separately
frcroth Dec 19, 2022
79f13d6
Fix usage of legacy credentials and no credentials
frcroth Dec 19, 2022
e97c976
Fix schema
frcroth Dec 19, 2022
587104a
Merge branch 'master' into store-credentials-separately
frcroth Dec 19, 2022
dec5fe5
Update schema number and migrations, changelog
frcroth Dec 19, 2022
2bed418
Make match exhaustive
frcroth Dec 19, 2022
3909cb8
Use protected and private
frcroth Dec 19, 2022
a71094c
Merge branch 'master' into store-credentials-separately
fm3 Jan 2, 2023
2e7d09b
Merge branch 'master' into store-credentials-separately
frcroth Jan 5, 2023
c06ba4b
Use SecuredSQLDAO instead of SQLDAO
frcroth Jan 5, 2023
d814d35
Merge branch 'master' into store-credentials-separately
frcroth Jan 5, 2023
aee248e
Update schema number
frcroth Jan 5, 2023
338a09b
Merge branch 'master' into store-credentials-separately
frcroth Jan 17, 2023
60b019e
Fix error in findOne
frcroth Jan 17, 2023
e9df5a3
Merge branch 'master' into store-credentials-separately
frcroth Jan 17, 2023
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 @@ -16,6 +16,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Precomputed meshes can now be loaded even when a mapping is active (HDF5 or an editable mapping produced by the proofreading tool). The precomputed mesh has to be computed without a mapping for this to work. [#6569](https://github.com/scalableminds/webknossos/pull/6569)

### Changed
- For remote datasets that require authentication, credentials are no longer stored in the respective JSON. [#6646](https://github.com/scalableminds/webknossos/pull/6646)
- Improved performance of opening a dataset or annotation. [#6711](https://github.com/scalableminds/webknossos/pull/6711)
- Redesigned organization page to include more infos on organization users, storage, webKnossos plan and provided opportunities to upgrade. [#6602](https://github.com/scalableminds/webknossos/pull/6602)
- Changed branding of WEBKNOSSOS including a new logo, new primary colors, and UPPERCASE name. [#6739](https://github.com/scalableminds/webknossos/pull/6739)
Expand Down
1 change: 1 addition & 0 deletions MIGRATIONS.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ User-facing changes are documented in the [changelog](CHANGELOG.released.md).
- [094-pricing-plans.sql](conf/evolutions/reversions/094-pricing-plans.sql)
- [095-constraint-naming.sql](conf/evolutions/reversions/095-constraint-naming.sql)
- [096-storage.sql](conf/evolutions/096-storage.sql)
- [097-credentials.sql](conf/evolutions/097-credentials.sql)
65 changes: 65 additions & 0 deletions app/controllers/CredentialController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package controllers

import com.mohiva.play.silhouette.api.Silhouette
import com.scalableminds.util.tools.FoxImplicits
import com.scalableminds.webknossos.datastore.storage.{HttpBasicAuthCredential, S3AccessKeyCredential}
import models.binary.credential.CredentialDAO
import oxalis.security.WkEnv
import play.api.libs.json.{Json, OFormat}
import play.api.mvc.{Action, PlayBodyParsers}
import utils.ObjectId

import javax.inject.Inject
import scala.concurrent.ExecutionContext

case class HttpBasicAuthCredentialParameters(name: String, username: String, password: String, domain: Option[String])

object HttpBasicAuthCredentialParameters {
implicit val jsonFormat: OFormat[HttpBasicAuthCredentialParameters] = Json.format[HttpBasicAuthCredentialParameters]
}

case class S3AccessKeyCredentialParameters(name: String, keyId: String, key: String, bucket: Option[String])

object S3AccessKeyCredentialParameters {
implicit val jsonFormat: OFormat[S3AccessKeyCredentialParameters] = Json.format[S3AccessKeyCredentialParameters]
}

class CredentialController @Inject()(credentialDAO: CredentialDAO, sil: Silhouette[WkEnv])(
implicit ec: ExecutionContext,
val bodyParsers: PlayBodyParsers)
extends Controller
with FoxImplicits {

def createHttpBasicAuthCredential: Action[HttpBasicAuthCredentialParameters] =
sil.SecuredAction.async(validateJson[HttpBasicAuthCredentialParameters]) { implicit request =>
val _id = ObjectId.generate
for {
_ <- bool2Fox(request.identity.isAdmin) ?~> "notAllowed" ~> FORBIDDEN
_ <- credentialDAO.insertOne(
_id,
HttpBasicAuthCredential(request.body.name,
request.body.username,
request.body.password,
request.identity._id.toString,
request.identity._organization.toString)
) ?~> "create.failed"
} yield Ok(Json.toJson(_id))
}

def createS3AccessKeyCredential: Action[S3AccessKeyCredentialParameters] =
sil.SecuredAction.async(validateJson[S3AccessKeyCredentialParameters]) { implicit request =>
val _id = ObjectId.generate
for {
_ <- bool2Fox(request.identity.isAdmin) ?~> "notAllowed" ~> FORBIDDEN
_ <- credentialDAO.insertOne(
_id,
S3AccessKeyCredential(request.body.name,
request.body.keyId,
request.body.key,
request.identity._id.toString,
request.identity._organization.toString)
) ?~> "create.failed"
} yield Ok(Json.toJson(_id))
}

}
2 changes: 1 addition & 1 deletion app/controllers/DataSetController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class DataSetController @Inject()(userService: UserService,
val reportMutable = ListBuffer[String]()
for {
dataSourceBox: Box[GenericDataSource[DataLayer]] <- exploreRemoteLayerService
.exploreRemoteDatasource(request.body, reportMutable)
.exploreRemoteDatasource(request.body, request.identity, reportMutable)
.futureBox
dataSourceOpt = dataSourceBox match {
case Full(dataSource) if dataSource.dataLayers.nonEmpty =>
Expand Down
14 changes: 13 additions & 1 deletion app/controllers/WKRemoteDataStoreController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import com.scalableminds.webknossos.datastore.services.{
ReserveUploadInformation
}
import com.typesafe.scalalogging.LazyLogging
import javax.inject.Inject
import models.analytics.{AnalyticsService, UploadDatasetEvent}
import models.binary._
import models.binary.credential.CredentialDAO
import models.folder.FolderDAO
import models.job.JobDAO
import models.organization.OrganizationDAO
Expand All @@ -26,7 +28,6 @@ import play.api.libs.json.{JsError, JsSuccess, JsValue, Json}
import play.api.mvc.{Action, AnyContent, PlayBodyParsers}
import utils.ObjectId

import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class WKRemoteDataStoreController @Inject()(
Expand All @@ -41,6 +42,7 @@ class WKRemoteDataStoreController @Inject()(
userDAO: UserDAO,
folderDAO: FolderDAO,
jobDAO: JobDAO,
credentialDAO: CredentialDAO,
mailchimpClient: MailchimpClient,
wkSilhouetteEnvironment: WkSilhouetteEnvironment)(implicit ec: ExecutionContext, bodyParsers: PlayBodyParsers)
extends Controller
Expand Down Expand Up @@ -192,4 +194,14 @@ class WKRemoteDataStoreController @Inject()(
}
}

def findCredential(name: String, key: String, credentialId: String): Action[AnyContent] = Action.async {
implicit request =>
dataStoreService.validateAccess(name, key) { _ =>
for {
credentialIdValidated <- ObjectId.fromString(credentialId)
credential <- credentialDAO.findOne(credentialIdValidated)
} yield Ok(Json.toJson(credential))
}
}

}
69 changes: 69 additions & 0 deletions app/models/binary/credential/CredentialDAO.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package models.binary.credential

import com.scalableminds.util.tools.Fox
import com.scalableminds.webknossos.datastore.storage.{AnyCredential, HttpBasicAuthCredential, S3AccessKeyCredential}
import com.scalableminds.webknossos.schema.Tables.{Credentials, CredentialsRow}
import utils.sql.{SecuredSQLDAO, SqlClient, SqlToken}
import utils.ObjectId

import javax.inject.Inject
import scala.concurrent.ExecutionContext

class CredentialDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext) extends SecuredSQLDAO(sqlClient) {
protected val collection = Credentials

protected def columnsList: List[String] = collection.baseTableRow.create_*.map(_.name).toList
override protected def collectionName: String = "credentials"
def columns: SqlToken = SqlToken.raw(columnsList.mkString(", "))

private def parseAsHttpBasicAuthCredential(r: CredentialsRow): Fox[HttpBasicAuthCredential] =
for {
username <- r.identifier.toFox
password <- r.secret.toFox
} yield
HttpBasicAuthCredential(
r.name,
username,
password,
r._User,
r._Organization
)

private def parseAsS3AccessKeyCredential(r: CredentialsRow): Fox[S3AccessKeyCredential] =
for {
keyId <- r.identifier.toFox
key <- r.secret.toFox
} yield
S3AccessKeyCredential(
r.name,
keyId,
key,
r._User,
r._Organization
)

def insertOne(_id: ObjectId, credential: HttpBasicAuthCredential): Fox[Unit] =
for {
_ <- run(q"""insert into webknossos.credentials(_id, type, name, identifier, secret, _user, _organization)
values(${_id}, ${CredentialType.HTTP_Basic_Auth}, ${credential.name}, ${credential.username}, ${credential.password}, ${credential.user}, ${credential.organization})""".asUpdate)
} yield ()

def insertOne(_id: ObjectId, credential: S3AccessKeyCredential): Fox[Unit] =
for {
_ <- run(q"""insert into webknossos.credentials(_id, type, name, identifier, secret, _user, _organization)
values(${_id}, ${CredentialType.S3_Access_Key}, ${credential.name}, ${credential.keyId}, ${credential.key}, ${credential.user}, ${credential.organization})""".asUpdate)
} yield ()

def findOne(id: ObjectId): Fox[AnyCredential] =
for {
r <- run(q"select $columns from webknossos.credentials_ where _id = $id".as[CredentialsRow])
firstRow <- r.headOption.toFox
parsed <- parseAnyCredential(firstRow)
} yield parsed

private def parseAnyCredential(r: CredentialsRow): Fox[AnyCredential] =
r.`type` match {
case "HTTP_Basic_Auth" => parseAsHttpBasicAuthCredential(r)
case "S3_Access_Key" => parseAsS3AccessKeyCredential(r)
}
}
51 changes: 51 additions & 0 deletions app/models/binary/credential/CredentialService.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package models.binary.credential

import com.scalableminds.util.tools.Fox
import com.scalableminds.webknossos.datastore.storage.{HttpBasicAuthCredential, S3AccessKeyCredential}
import utils.ObjectId

import java.net.URI
import javax.inject.Inject
import scala.concurrent.ExecutionContext

class CredentialService @Inject()(credentialDao: CredentialDAO) {

def createCredential(uri: URI,
username: Option[String],
password: Option[String],
user: String,
organization: String)(implicit ec: ExecutionContext): Fox[Option[ObjectId]] = {
val scheme = uri.getScheme
scheme match {
case "https" =>
username match {
case Some(u) =>
val _id = ObjectId.generate
for {
_ <- credentialDao.insertOne(
_id,
HttpBasicAuthCredential(uri.toString, u, password.getOrElse(""), user, organization))
_ <- credentialDao.findOne(_id)
} yield Some(_id)
case None => Fox.empty
}
case "s3" =>
username match {
case Some(keyId) =>
password match {
case Some(secretKey) =>
val _id = ObjectId.generate
for {
_ <- credentialDao.insertOne(
_id,
S3AccessKeyCredential(uri.toString, keyId, secretKey, user, organization))
_ <- credentialDao.findOne(_id)
} yield Some(_id)
case None => Fox.empty
}
case None => Fox.empty
}
}
}

}
9 changes: 9 additions & 0 deletions app/models/binary/credential/CredentialType.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package models.binary.credential

import com.scalableminds.util.enumeration.ExtendedEnumeration

object CredentialType extends ExtendedEnumeration {
type CredentialType = Value

val HTTP_Basic_Auth, S3_Access_Key, HTTP_Token, GCS = Value
}
33 changes: 24 additions & 9 deletions app/models/binary/explore/ExploreRemoteLayerService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import com.scalableminds.webknossos.datastore.datareaders.zarr._
import com.scalableminds.webknossos.datastore.models.datasource._
import com.scalableminds.webknossos.datastore.storage.FileSystemsHolder
import com.typesafe.scalalogging.LazyLogging
import models.binary.credential.CredentialService
import models.user.User
import net.liftweb.common.{Box, Empty, Failure, Full}
import net.liftweb.util.Helpers.tryo
import oxalis.security.WkEnv
import play.api.libs.json.{Json, OFormat}

import java.net.URI
Expand All @@ -26,14 +29,20 @@ object ExploreRemoteDatasetParameters {
implicit val jsonFormat: OFormat[ExploreRemoteDatasetParameters] = Json.format[ExploreRemoteDatasetParameters]
}

class ExploreRemoteLayerService @Inject()() extends FoxImplicits with LazyLogging {
class ExploreRemoteLayerService @Inject()(credentialService: CredentialService) extends FoxImplicits with LazyLogging {

def exploreRemoteDatasource(
urisWithCredentials: List[ExploreRemoteDatasetParameters],
requestIdentity: WkEnv#I,
reportMutable: ListBuffer[String])(implicit ec: ExecutionContext): Fox[GenericDataSource[DataLayer]] =
for {
exploredLayersNested <- Fox.serialCombined(urisWithCredentials)(parameters =>
exploreRemoteLayersForUri(parameters.remoteUri, parameters.user, parameters.password, reportMutable))
exploredLayersNested <- Fox.serialCombined(urisWithCredentials)(
parameters =>
exploreRemoteLayersForUri(parameters.remoteUri,
parameters.user,
parameters.password,
reportMutable,
requestIdentity))
layersWithVoxelSizes = exploredLayersNested.flatten
_ <- bool2Fox(layersWithVoxelSizes.nonEmpty) ?~> "Detected zero layers"
rescaledLayersAndVoxelSize <- rescaleLayersByCommonVoxelSize(layersWithVoxelSizes) ?~> "Could not extract common voxel size from layers"
Expand Down Expand Up @@ -131,14 +140,20 @@ class ExploreRemoteLayerService @Inject()() extends FoxImplicits with LazyLoggin
layerUri: String,
user: Option[String],
password: Option[String],
reportMutable: ListBuffer[String])(implicit ec: ExecutionContext): Fox[List[(DataLayer, Vec3Double)]] =
reportMutable: ListBuffer[String],
requestingUser: User)(implicit ec: ExecutionContext): Fox[List[(DataLayer, Vec3Double)]] =
for {
remoteSource <- tryo(RemoteSourceDescriptor(new URI(normalizeUri(layerUri)), user, password)).toFox ?~> s"Received invalid URI: $layerUri"
credentialId <- credentialService.createCredential(new URI(normalizeUri(layerUri)),
user,
password,
requestingUser._id.toString,
requestingUser._organization.toString)
fileSystem <- FileSystemsHolder.getOrCreate(remoteSource).toFox ?~> "Failed to set up remote file system"
remotePath <- tryo(fileSystem.getPath(remoteSource.remotePath)) ?~> "Failed to get remote path"
layersWithVoxelSizes <- exploreRemoteLayersForRemotePath(
remotePath,
remoteSource.credentials,
credentialId.map(_.toString),
reportMutable,
List(new ZarrArrayExplorer, new NgffExplorer, new N5ArrayExplorer, new N5MultiscalesExplorer))
} yield layersWithVoxelSizes
Expand All @@ -153,23 +168,23 @@ class ExploreRemoteLayerService @Inject()() extends FoxImplicits with LazyLoggin

private def exploreRemoteLayersForRemotePath(
remotePath: Path,
credentials: Option[FileSystemCredentials],
credentialId: Option[String],
reportMutable: ListBuffer[String],
explorers: List[RemoteLayerExplorer])(implicit ec: ExecutionContext): Fox[List[(DataLayer, Vec3Double)]] =
explorers match {
case Nil => Fox.empty
case currentExplorer :: remainingExplorers =>
reportMutable += s"\nTrying to explore $remotePath as ${currentExplorer.name}..."
currentExplorer.explore(remotePath, credentials).futureBox.flatMap {
currentExplorer.explore(remotePath, credentialId).futureBox.flatMap {
case Full(layersWithVoxelSizes) =>
reportMutable += s"Found ${layersWithVoxelSizes.length} ${currentExplorer.name} layers at $remotePath."
Fox.successful(layersWithVoxelSizes)
case f: Failure =>
reportMutable += s"Error when reading $remotePath as ${currentExplorer.name}: ${formatFailureForReport(f)}"
exploreRemoteLayersForRemotePath(remotePath, credentials, reportMutable, remainingExplorers)
exploreRemoteLayersForRemotePath(remotePath, credentialId, reportMutable, remainingExplorers)
case Empty =>
reportMutable += s"Error when reading $remotePath as ${currentExplorer.name}: Empty"
exploreRemoteLayersForRemotePath(remotePath, credentials, reportMutable, remainingExplorers)
exploreRemoteLayersForRemotePath(remotePath, credentialId, reportMutable, remainingExplorers)
}
}

Expand Down
5 changes: 2 additions & 3 deletions app/models/binary/explore/N5ArrayExplorer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import com.scalableminds.util.geometry.{Vec3Double, Vec3Int}
import com.scalableminds.util.tools.Fox
import com.scalableminds.webknossos.datastore.dataformats.MagLocator
import com.scalableminds.webknossos.datastore.dataformats.n5.{N5DataLayer, N5Layer, N5SegmentationLayer}
import com.scalableminds.webknossos.datastore.dataformats.zarr.FileSystemCredentials
import com.scalableminds.webknossos.datastore.datareaders.AxisOrder
import com.scalableminds.webknossos.datastore.datareaders.n5.N5Header
import com.scalableminds.webknossos.datastore.models.datasource.Category
Expand All @@ -15,15 +14,15 @@ class N5ArrayExplorer extends RemoteLayerExplorer {

override def name: String = "N5 Array"

override def explore(remotePath: Path, credentials: Option[FileSystemCredentials]): Fox[List[(N5Layer, Vec3Double)]] =
override def explore(remotePath: Path, credentialId: Option[String]): Fox[List[(N5Layer, Vec3Double)]] =
for {
headerPath <- Fox.successful(remotePath.resolve(N5Header.FILENAME_ATTRIBUTES_JSON))
name <- guessNameFromPath(remotePath)
n5Header <- parseJsonFromPath[N5Header](headerPath) ?~> s"failed to read n5 header at $headerPath"
elementClass <- n5Header.elementClass ?~> "failed to read element class from n5 header"
guessedAxisOrder = AxisOrder.asZyxFromRank(n5Header.rank)
boundingBox <- n5Header.boundingBox(guessedAxisOrder) ?~> "failed to read bounding box from zarr header. Make sure data is in (T/C)ZYX format"
magLocator = MagLocator(Vec3Int.ones, Some(remotePath.toString), credentials, Some(guessedAxisOrder), None)
magLocator = MagLocator(Vec3Int.ones, Some(remotePath.toString), None, Some(guessedAxisOrder), None, credentialId)
layer: N5Layer = if (looksLikeSegmentationLayer(name, elementClass)) {
N5SegmentationLayer(name, boundingBox, elementClass, List(magLocator), largestSegmentId = None)
} else N5DataLayer(name, Category.color, boundingBox, elementClass, List(magLocator))
Expand Down
Loading