Skip to content

Commit

Permalink
Add more web permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
Krisjanis Veinbahs committed Mar 14, 2022
1 parent 11eb6e7 commit 2d0d9e3
Show file tree
Hide file tree
Showing 15 changed files with 491 additions and 90 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package diptestbed.database.services

import cats.data.EitherT
import scala.concurrent.ExecutionContext
import cats.effect.Async
import cats.implicits._
import diptestbed.database.catalog.HardwareAccessCatalog.HardwareAccessTable
import diptestbed.database.catalog.HardwareAccessCatalog.{HardwareAccessRow, HardwareAccessTable}
import diptestbed.database.catalog.HardwareCatalog.{HardwareRow, HardwareTable, toDomain => hardwareToDomain}
import diptestbed.database.catalog.UserCatalog.UserTable
import diptestbed.database.driver.DatabaseDriverOps._
import diptestbed.database.driver.DatabaseOutcome.DatabaseResult
import diptestbed.database.driver.DatabaseOutcome.{DatabaseException, DatabaseResult}
import diptestbed.domain.{Hardware, HardwareId, User, UserId}
import slick.dbio.DBIOAction.sequenceOption

Expand All @@ -19,7 +20,6 @@ class HardwareService[F[_]: Async](
import hardwareTable.dbDriver.profile.api._
import hardwareTable._
import hardwareAccessTable.HardwareAccessQuery
import hardwareAccessTable.HardwareAccessTable
import userTable.UserQuery

def countAllHardware(): F[DatabaseResult[Int]] =
Expand All @@ -42,7 +42,7 @@ class HardwareService[F[_]: Async](
.map(dbioAction => dbioAction.map(hardwareId => hardwareId.map(_ => hardwareToDomain(row))))
}

def accessibleHardwareQuery(requester: Option[User]): Query[hardwareTable.HardwareTable, HardwareRow, Seq] =
def accessibleHardwareQuery(requester: Option[User], write: Boolean): Query[hardwareTable.HardwareTable, HardwareRow, Seq] =
requester match {
case None => HardwareQuery
case Some(user) if user.isManager => HardwareQuery
Expand All @@ -52,24 +52,81 @@ class HardwareService[F[_]: Async](
.joinLeft(HardwareAccessQuery)
.on((h, a) => h.uuid === a.hardwareId)
.filter { case (h, a) =>
a.map(_.userId) === user.id.value || h.ownerUuid === user.id.value
(h.isPublic && !write) || a.map(_.userId).filter(_ => !write) === user.id.value || h.ownerUuid === user.id.value
}
.distinctOn { case (h, _) => h.uuid }
.map { case (h, _) => h }
}

def getHardware(requester: Option[User], id: HardwareId): F[DatabaseResult[Option[Hardware]]] =
accessibleHardwareQuery(requester)
def getHardware(requester: Option[User], id: HardwareId, write: Boolean): F[DatabaseResult[Option[Hardware]]] =
accessibleHardwareQuery(requester, write)
.filter(_.uuid === id.value)
.result
.headOption
.map(_.map(hardwareToDomain))
.tryRunDBIO(dbDriver)

def getHardwares(requester: Option[User]): F[DatabaseResult[Seq[Hardware]]] = {
accessibleHardwareQuery(requester)
def getHardwares(requester: Option[User], write: Boolean): F[DatabaseResult[Seq[Hardware]]] = {
accessibleHardwareQuery(requester, write)
.result
.map(_.map(hardwareToDomain))
.tryRunDBIO(dbDriver)
}

def getManageableHardware(manager: Option[User], accessUserId: UserId): F[DatabaseResult[Seq[(Hardware, Boolean)]]] = {
accessibleHardwareQuery(manager, write = false)
.joinLeft(HardwareAccessQuery)
.on((h, a) => h.uuid === a.hardwareId && a.userId === accessUserId.value)
.distinctOn { case (h, _) => h.uuid }
.map { case (h, a) => (h, a.isDefined || h.isPublic) }
.result
.map(_.map { case (h, a) => (hardwareToDomain(h), a) })
.tryRunDBIO(dbDriver)
}

def setHardwareAccess(
requester: Option[User],
userId: UserId,
hardwareId: HardwareId,
isAccessible: Boolean
): F[DatabaseResult[Int]] = {
val additionQuery = HardwareAccessQuery += HardwareAccessRow(hardwareId.value, userId.value)
val deletionQuery =
HardwareAccessQuery
.filter(a => a.userId === userId.value && a.hardwareId === hardwareId.value)
.delete

if (isAccessible) {
requester match {
case None => additionQuery.tryRunDBIO(dbDriver)
case Some(user) if user.isManager => additionQuery.tryRunDBIO(dbDriver)
case Some(user) =>
(for {
requesterAccessibleHardwareId <- EitherT(getHardware(Some(user), hardwareId, write = true))
_ <- EitherT.fromEither[F](Either.cond(requesterAccessibleHardwareId.isDefined, (), DatabaseException(new Exception("Entity already exists"))))
userCreation <- EitherT(additionQuery.tryRunDBIO(dbDriver))
} yield userCreation).value
}
} else {
requester match {
case None => deletionQuery.tryRunDBIO(dbDriver)
case Some(user) if user.isManager => deletionQuery.tryRunDBIO(dbDriver)
case Some(user) =>
(for {
requesterAccessibleHardwareId <- EitherT(getHardware(Some(user), hardwareId, write = true))
_ <- EitherT.fromEither[F](Either.cond(requesterAccessibleHardwareId.isDefined, (), DatabaseException(new Exception("Entity already exists"))))
deletion <- EitherT(deletionQuery.tryRunDBIO(dbDriver))
} yield deletion).value

}
}
}

def setPublic(requester: Option[User], id: HardwareId, isPublic: Boolean): EitherT[F, DatabaseException, Int] = {
for {
requesterAccessibleHardware <- EitherT(getHardware(requester, id, write = true))
_ <- EitherT.fromEither[F](Either.cond(requesterAccessibleHardware.isDefined, (), DatabaseException(new Exception("Entity not managable"))))
result <- EitherT(HardwareQuery.filter(_.uuid === id.value).map(_.isPublic).update(isPublic).tryRunDBIO(dbDriver))
} yield result
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package diptestbed.database.services

import cats.data.EitherT
import cats.effect.Async
import cats.implicits._
import diptestbed.database.catalog.SoftwareCatalog.{SoftwareRow, SoftwareTable}
import diptestbed.database.catalog.UserCatalog.UserTable
import diptestbed.database.driver.DatabaseDriverOps._
import diptestbed.database.driver.DatabaseOutcome.DatabaseResult
import diptestbed.database.driver.DatabaseOutcome.{DatabaseException, DatabaseResult}
import diptestbed.domain.{Software, SoftwareId, SoftwareMeta, User, UserId}
import slick.dbio.DBIOAction.sequenceOption

import scala.concurrent.ExecutionContext

class SoftwareService[F[_]: Async](
Expand Down Expand Up @@ -44,25 +44,25 @@ class SoftwareService[F[_]: Async](
)
}

def accessibleSoftwareQuery(requester: Option[User]): Query[softwareTable.SoftwareTable, SoftwareRow, Seq] =
def accessibleSoftwareQuery(requester: Option[User], write: Boolean): Query[softwareTable.SoftwareTable, SoftwareRow, Seq] =
requester match {
// If no requester, then assuming full accessibility
case None => SoftwareQuery
case Some(user) if user.isManager => SoftwareQuery
case Some(user) => SoftwareQuery.filter(s => s.ownerUuid === user.id.value)
case Some(user) => SoftwareQuery.filter(s => (s.isPublic && !write) || s.ownerUuid === user.id.value)
}

def getSoftware(requester: Option[User], id: SoftwareId): F[DatabaseResult[Option[Software]]] =
accessibleSoftwareQuery(requester)
def getSoftware(requester: Option[User], id: SoftwareId, write: Boolean): F[DatabaseResult[Option[Software]]] =
accessibleSoftwareQuery(requester, write)
.filter(_.uuid === id.value)
.result
.headOption
.map(_.map(row =>
Software(SoftwareMeta(SoftwareId(row.id), UserId(row.ownerId), row.name, row.isPublic), row.content)))
.tryRunDBIO(dbDriver)

def getSoftwareMeta(requester: Option[User], id: SoftwareId): F[DatabaseResult[Option[SoftwareMeta]]] =
accessibleSoftwareQuery(requester)
def getSoftwareMeta(requester: Option[User], id: SoftwareId, write: Boolean): F[DatabaseResult[Option[SoftwareMeta]]] =
accessibleSoftwareQuery(requester, write)
.filter(_.uuid === id.value)
.map(row => (row.uuid, row.ownerUuid, row.name, row.isPublic))
.result
Expand All @@ -73,8 +73,8 @@ class SoftwareService[F[_]: Async](
})
.tryRunDBIO(dbDriver)

def getSoftwareMetas(requester: Option[User]): F[DatabaseResult[Seq[SoftwareMeta]]] =
accessibleSoftwareQuery(requester)
def getSoftwareMetas(requester: Option[User], write: Boolean): F[DatabaseResult[Seq[SoftwareMeta]]] =
accessibleSoftwareQuery(requester, write)
.map(row => (row.uuid, row.ownerUuid, row.name, row.isPublic))
.result
.map(_.map {
Expand All @@ -83,4 +83,11 @@ class SoftwareService[F[_]: Async](
})
.tryRunDBIO(dbDriver)

def setPublic(requester: Option[User], id: SoftwareId, isPublic: Boolean): EitherT[F, DatabaseException, Int] = {
for {
requesterAccessibleSoftware <- EitherT(getSoftwareMeta(requester, id, write = true))
_ <- EitherT.fromEither[F](Either.cond(requesterAccessibleSoftware.isDefined, (), DatabaseException(new Exception("Entity not managable"))))
result <- EitherT(SoftwareQuery.filter(_.uuid === id.value).map(_.isPublic).update(isPublic).tryRunDBIO(dbDriver))
} yield result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class UserService[F[_]: Async](
requester match {
// If no requester, then assuming full accessibility
case None => UserQuery
case Some(user) if user.isManager => UserQuery
case Some(user) if user.isManager || user.isLabOwner => UserQuery
case Some(user) => UserQuery.filter(s => s.uuid === user.id.value)
}

Expand Down
17 changes: 13 additions & 4 deletions backend/web/app/diptestbed/web/AppRouter.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package diptestbed.web

import controllers.Assets
import diptestbed.domain.UserId
import diptestbed.web.control.{AppAuthController, AppHardwareController, AppHomeController, AppSoftwareController, AppUserController}
import play.api.routing.Router.Routes
import play.api.routing.SimpleRouter
import play.api.routing.sird._
import diptestbed.web.sird.Binders._

class AppRouter(
assets: Assets,
Expand All @@ -25,9 +27,16 @@ class AppRouter(
case POST(p"/register") => appAuthController.registerRequest
case GET(p"/logout") => appAuthController.logout

case GET(p"/hardware") => appHardwareController.list
case GET(p"/software") => appSoftwareController.list
case GET(p"/user") => appUserController.list
case POST(p"/user/permissions") => appUserController.permissionsRequest
case GET(p"/hardware") => appHardwareController.list
case POST(p"/hardware/public") => appHardwareController.publicRequest

case GET(p"/software") => appSoftwareController.list
case POST(p"/software/public") => appSoftwareController.publicRequest

case GET(p"/user") => appUserController.list
case GET(p"/user/${uuid(userId)}") => appUserController.view(UserId(userId))
case POST(p"/user/permissions") => appUserController.permissionsRequest(None)
case POST(p"/user/permissions/${uuid(userId)}") => appUserController.permissionsRequest(Some(UserId(userId)))
case POST(p"/user/hardware-access/${uuid(userId)}") => appUserController.hardwareAccessRequest(UserId(userId))
}
}
3 changes: 2 additions & 1 deletion backend/web/app/diptestbed/web/DIPTestbedModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ class DIPTestbedModule(context: Context)(implicit iort: IORuntime)
lazy val appUserController = new AppUserController(
appConfig,
controllerComponents,
userService
userService,
hardwareService,
)

lazy val appRouter = new AppRouter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class ApiHardwareController(
for {
_ <- EitherT.fromEither[IO](Either.cond(
user.canAccessHardware, (), permissionErrorResult("Hardware access")))
hardwares <- EitherT(hardwareService.getHardwares(Some(user))).leftMap(databaseErrorResult)
hardwares <- EitherT(hardwareService.getHardwares(Some(user), write = false)).leftMap(databaseErrorResult)
result = Success(hardwares).withHttpStatus(OK)
} yield result
))
Expand All @@ -77,7 +77,7 @@ class ApiHardwareController(
for {
_ <- EitherT.fromEither[IO](Either.cond(
user.canAccessHardware, (), permissionErrorResult("Hardware access")))
hardware <- EitherT(hardwareService.getHardware(Some(user), hardwareId)).leftMap(databaseErrorResult)
hardware <- EitherT(hardwareService.getHardware(Some(user), hardwareId, write = false)).leftMap(databaseErrorResult)
_ <- EitherT.fromEither[IO](hardware.toRight(unknownIdErrorResult))
result = Success(hardware).withHttpStatus(OK)
} yield result
Expand All @@ -98,7 +98,7 @@ class ApiHardwareController(
IOActionAny(withRequestAuthnOrFail(_)((_, user) => {
implicit val timeout: Timeout = 60.seconds
for {
hardware <- EitherT(hardwareService.getHardware(Some(user), hardwareId)).leftMap(databaseErrorResult)
hardware <- EitherT(hardwareService.getHardware(Some(user), hardwareId, write = false)).leftMap(databaseErrorResult)
_ <- EitherT.fromEither[IO](hardware.toRight(unknownIdErrorResult))
_ <- EitherT.fromEither[IO](Either.cond(
user.canAccessHardware, (), permissionErrorResult("Hardware access")))
Expand Down Expand Up @@ -143,7 +143,7 @@ class ApiHardwareController(
def cameraSink(hardwareId: HardwareId): Action[AnyContent] =
IOActionAny(withRequestAuthnOrFail(_)((request, user) => {
for {
hardware <- EitherT(hardwareService.getHardware(Some(user), hardwareId)).leftMap(databaseErrorResult)
hardware <- EitherT(hardwareService.getHardware(Some(user), hardwareId, write = false)).leftMap(databaseErrorResult)
_ <- EitherT.fromEither[IO](hardware.toRight(unknownIdErrorResult))
_ <- EitherT.fromEither[IO](Either.cond(
user.canAccessHardware, (), permissionErrorResult("Hardware access")))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class ApiSoftwareController(
for {
_ <- EitherT.fromEither[IO](Either.cond(
user.canAccessSoftware, (), permissionErrorResult("Software access")))
result <- EitherT(softwareService.getSoftwareMetas(Some(user)))
result <- EitherT(softwareService.getSoftwareMetas(Some(user), write = false))
.leftMap(databaseErrorResult)
.map(softwareMetas => Success(softwareMetas).withHttpStatus(OK))
} yield result
Expand All @@ -79,7 +79,7 @@ class ApiSoftwareController(
for {
_ <- EitherT.fromEither[IO](Either.cond(
user.canAccessSoftware, (), permissionErrorResult("Software access")))
software <- EitherT(softwareService.getSoftware(Some(user), softwareId)).leftMap(databaseErrorResult)
software <- EitherT(softwareService.getSoftware(Some(user), softwareId, write = false)).leftMap(databaseErrorResult)
existingSoftware <- EitherT.fromEither[IO](software.toRight(unknownIdErrorResult))
result = {
val tempFile = File.makeTemp()
Expand Down
50 changes: 45 additions & 5 deletions backend/web/app/diptestbed/web/control/AppHardwareController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,70 @@ import cats.effect.IO
import cats.effect.unsafe.IORuntime
import cats.implicits.toTraverseOps
import diptestbed.database.services.{HardwareService, UserService}
import diptestbed.domain.DIPTestbedConfig
import diptestbed.domain.{DIPTestbedConfig, Hardware, HardwareId, User}
import diptestbed.web.control.FormHelper.optionalBoolean
import diptestbed.web.ioControls.IOController
import play.api.data.Form
import play.api.data.Forms.{text, tuple}
import play.api.i18n.Messages
import play.api.mvc._
import java.util.UUID
import scala.util.Try

class AppHardwareController(
val appConfig: DIPTestbedConfig,
val cc: ControllerComponents,
val hardwareService: HardwareService[IO],
val userService: UserService[IO]
)(implicit
iort: IORuntime
iort: IORuntime,
messages: Messages
) extends AbstractController(cc)
with IOController
with ResultsController[IO]
with AuthController[IO] {
implicit val ac: DIPTestbedConfig = appConfig
def setPublic(
sessionUser: User,
hardwareId: String,
isPublic: Boolean
): Boolean =
Try(UUID.fromString(hardwareId)).toOption.map(HardwareId)
.traverse(id =>
hardwareService.setPublic(Some(sessionUser), id, isPublic)).value.map(_.toOption.isDefined).unsafeRunSync()

def publicForm(sessionUser: User): Form[(String, Boolean)] = Form[(String, Boolean)](
tuple(
"hardware_id" -> text,
"is_public" -> optionalBoolean,
) verifying ("Failed to change permissions", {
case (hardwareId, isPublic) =>
setPublic(sessionUser, hardwareId, isPublic)
})
)

def dbHardware(sessionUser: User): IO[List[Hardware]] =
hardwareService.getHardwares(Some(sessionUser), write = false)
.map(_.toOption.sequence.flatten.toList.filter(_ => sessionUser.canAccessHardware))

def list: Action[AnyContent] =
Action { implicit request =>
withRequestAuthnOrLoginRedirect[AnyContent] { case (_, user) =>
val hardwareList = hardwareService.getHardwares(Some(user))
.map(_.toOption.sequence.flatten.toList.filter(_ => user.canAccessHardware))
Ok(diptestbed.web.views.html.hardwareList(
appConfig, Some(user), hardwareList.unsafeRunSync()))
appConfig, Some(user), dbHardware(user).unsafeRunSync(), publicForm(user)))
}.unsafeRunSync()
}


def publicRequest: Action[AnyContent] =
Action { implicit request =>
withRequestAuthnOrLoginRedirect[AnyContent] { case (_, user) =>
publicForm(user).bindFromRequest.fold(
formWithErrors => BadRequest(diptestbed.web.views.html.hardwareList(
appConfig, Some(user), dbHardware(user).unsafeRunSync(), formWithErrors)),
_ => Ok(diptestbed.web.views.html.hardwareList(
appConfig, Some(user), dbHardware(user).unsafeRunSync(), publicForm(user)))
)
}.unsafeRunSync()
}
}
Loading

0 comments on commit 2d0d9e3

Please sign in to comment.