MiniDigger/Hangar

View on GitHub
orePlayCommon/app/controllers/sugar/Actions.scala

Summary

Maintainability
C
1 day
Test Coverage
package controllers.sugar

import scala.language.higherKinds

import java.time.Instant

import scala.concurrent.{ExecutionContext, Future}

import play.api.i18n.{Lang, Messages}
import play.api.mvc.Results.{Redirect, Unauthorized}
import play.api.mvc._

import controllers.OreControllerComponents
import controllers.sugar.Requests._
import db.impl.access.{OrganizationBase, ProjectBase, UserBase}
import ore.db.impl.OrePostgresDriver.api._
import ore.models.project.{Project, Visibility}
import ore.models.user.{SignOn, User}
import models.viewhelper._
import ore.OreConfig
import ore.auth.SSOApi
import ore.db.access.ModelView
import ore.db.{Model, ModelService}
import ore.models.organization.Organization
import ore.permission.Permission
import ore.permission.scope.{GlobalScope, HasScope}
import ore.util.OreMDC
import util.IOUtils

import cats.data.OptionT
import cats.effect.{ContextShift, IO}
import cats.syntax.all._
import com.typesafe.scalalogging

/**
  * A set of actions used by Ore.
  */
trait Actions extends Calls with ActionHelpers { self =>

  def oreComponents: OreControllerComponents[IO]

  implicit def service: ModelService[IO] = oreComponents.service
  def sso: SSOApi[IO]                    = oreComponents.sso
  def bakery: Bakery                     = oreComponents.bakery
  implicit def config: OreConfig                  = oreComponents.config

  implicit def users: UserBase[IO]                 = oreComponents.users
  implicit def projects: ProjectBase[IO]           = oreComponents.projects
  implicit def organizations: OrganizationBase[IO] = oreComponents.organizations

  implicit def ec: ExecutionContext = oreComponents.executionContext
  implicit def cs: ContextShift[IO] = IO.contextShift(ec)

  private val PermsLogger    = scalalogging.Logger("Permissions")
  private val MDCPermsLogger = scalalogging.Logger.takingImplicit[OreMDC](PermsLogger.underlying)

  val AuthTokenName = "_oretoken"

  /** Called when a [[User]] tries to make a request they do not have permission for */
  def onUnauthorized(implicit request: Request[_]): Future[Result] = {
    val noRedirect           = request.flash.get("noRedirect")
    implicit val mdc: OreMDC = OreMDC.NoMDC
    users.current.isEmpty
      .map { currentUserEmpty =>
        if (noRedirect.isEmpty && currentUserEmpty)
          Redirect(controllers.routes.Users.logIn(None, None, Some(request.path)))
        else
          Redirect(ShowHome)
      }
      .unsafeToFuture()
  }

  /**
    * Action to perform a permission check for the current ScopedRequest and
    * given Permission.
    *
    * @param p Permission to check
    * @tparam R Type of ScopedRequest that is being checked
    * @return The ScopedRequest as an instance of R
    */
  def PermissionAction[R[_] <: ScopedRequest[_]](
      p: Permission
  )(implicit ec: ExecutionContext, hasScope: HasScope[R[_]]): ActionRefiner[R, R] =
    new ActionRefiner[R, R] {
      def executionContext: ExecutionContext = ec

      private def log(success: Boolean, request: R[_]): Unit = {
        val lang = if (success) "GRANTED" else "DENIED"
        MDCPermsLogger.debug(s"<PERMISSION $lang> ${request.user.name}@${request.path.substring(1)}")(
          request: OreRequest[_]
        )
      }

      def refine[A](request: R[A]): Future[Either[Result, R[A]]] = {
        implicit val r: R[A] = request

        request.user.permissionsIn(request).map(_.has(p)).unsafeToFuture().flatMap { perm =>
          log(success = perm, request)
          if (!perm) onUnauthorized.map(Left.apply)
          else Future.successful(Right(request))
        }
      }
    }

  /**
    * A PermissionAction that uses an AuthedProjectRequest for the
    * ScopedRequest.
    *
    * @param p Permission to check
    * @return An [[ProjectRequest]]
    */
  def ProjectPermissionAction(p: Permission)(
      implicit ec: ExecutionContext
  ): ActionRefiner[AuthedProjectRequest, AuthedProjectRequest] = PermissionAction[AuthedProjectRequest](p)

  /**
    * A PermissionAction that uses an AuthedOrganizationRequest for the
    * ScopedRequest.
    *
    * @param p Permission to check
    * @return [[OrganizationRequest]]
    */
  def OrganizationPermissionAction(p: Permission)(
      implicit ec: ExecutionContext
  ): ActionRefiner[AuthedOrganizationRequest, AuthedOrganizationRequest] =
    PermissionAction[AuthedOrganizationRequest](p)

  implicit final class ResultWrapper(result: Result) {

    /**
      * Adds a new session cookie to the result for the specified [[User]].
      *
      * @param user   User to create session for
      * @param maxAge Maximum session age
      * @return Result with token
      */
    def authenticatedAs(user: User, maxAge: Int = -1): IO[Result] = {
      val session = users.createSession(user)
      val age     = if (maxAge == -1) None else Some(maxAge)
      session.map { s =>
        result.withCookies(bakery.bake(AuthTokenName, s.token, age))
      }
    }

    /**
      * Indicates that the client's session cookie should be cleared.
      *
      * @return
      */
    def clearingSession(): Result = result.discardingCookies(DiscardingCookie(AuthTokenName))

  }

  /**
    * Returns true and marks the nonce as used if the specified nonce has not
    * been used, has not expired.
    *
    * @param nonce Nonce to check
    * @return True if valid
    */
  def isNonceValid(nonce: String): IO[Boolean] =
    ModelView
      .now(SignOn)
      .find(_.nonce === nonce)
      .semiflatMap { signOn =>
        if (signOn.isCompleted || Instant.now().toEpochMilli - signOn.createdAt.toEpochMilli > 600000)
          IO.pure(false)
        else {
          service.update(signOn)(_.copy(isCompleted = true)).as(true)
        }
      }
      .exists(identity)

  /**
    * Returns a NotFound result with the 404 HTML template.
    *
    * @return NotFound
    */
  def notFound(implicit request: OreRequest[_]): Result

  // Implementation

  def userLock(redirect: Call)(implicit ec: ExecutionContext): ActionFilter[AuthRequest] =
    new ActionFilter[AuthRequest] {
      def executionContext: ExecutionContext = ec

      def filter[A](request: AuthRequest[A]): Future[Option[Result]] = Future.successful {
        if (!request.user.isLocked) None
        else Some(Redirect(redirect).withError("error.user.locked"))
      }
    }

  def verifiedAction(sso: Option[String], sig: Option[String])(
      implicit ec: ExecutionContext
  ): ActionFilter[AuthRequest] = new ActionFilter[AuthRequest] {
    def executionContext: ExecutionContext = ec

    def filter[A](request: AuthRequest[A]): Future[Option[Result]] = {
      val auth = for {
        ssoSome <- OptionT.fromOption[IO](sso)
        sigSome <- OptionT.fromOption[IO](sig)
        res     <- self.sso.authenticate(ssoSome, sigSome)(isNonceValid)
      } yield res

      auth
        .cata(
          Some(Unauthorized),
          spongeUser => if (spongeUser.id == request.user.id.value) None else Some(Unauthorized)
        )
        .unsafeToFuture()
    }
  }

  def userEditAction(username: String)(implicit ec: ExecutionContext, cs: ContextShift[IO]): ActionFilter[AuthRequest] =
    new ActionFilter[AuthRequest] {
      def executionContext: ExecutionContext = ec

      def filter[A](request: AuthRequest[A]): Future[Option[Result]] =
        users
          .requestPermission(request.user, username, Permission.EditOwnUserSettings)(request)
          .transform {
            case None    => Some(Unauthorized) // No Permission
            case Some(_) => None // Permission granted => No Filter
          }
          .value
          .unsafeToFuture()
    }

  def oreAction(
      implicit ec: ExecutionContext,
      cs: ContextShift[IO]
  ): ActionTransformer[Request, OreRequest] = new ActionTransformer[Request, OreRequest] {
    def executionContext: ExecutionContext = ec

    def transform[A](request: Request[A]): Future[OreRequest[A]] = {
      HeaderData
        .of(request)
        .map { data =>
          val requestWithLang =
            data.currentUser
              .flatMap(_.lang.map(Lang.apply))
              .fold(request)(lang => request.addAttr(Messages.Attrs.CurrentLang, lang))
          new SimpleOreRequest(data, requestWithLang)
        }
        .unsafeToFuture()
    }
  }

  def authAction(
      implicit ec: ExecutionContext,
      cs: ContextShift[IO]
  ): ActionRefiner[Request, AuthRequest] = new ActionRefiner[Request, AuthRequest] {
    def executionContext: ExecutionContext = ec

    def refine[A](request: Request[A]): Future[Either[Result, AuthRequest[A]]] =
      maybeAuthRequest(request, users.current(request, OreMDC.NoMDC))

  }

  private def maybeAuthRequest[A](
      request: Request[A],
      userF: OptionT[IO, Model[User]]
  )(implicit cs: ContextShift[IO]): Future[Either[Result, AuthRequest[A]]] =
    userF
      .semiflatMap(user => HeaderData.of(request).map(new AuthRequest(user, _, request)))
      .toRight(IO.fromFuture(IO(onUnauthorized(request))))
      .leftSemiflatMap(identity)
      .value
      .unsafeToFuture()

  def projectAction(author: String, slug: String)(
      implicit ec: ExecutionContext,
      cs: ContextShift[IO]
  ): ActionRefiner[OreRequest, ProjectRequest] = new ActionRefiner[OreRequest, ProjectRequest] {
    def executionContext: ExecutionContext = ec

    def refine[A](request: OreRequest[A]): Future[Either[Result, ProjectRequest[A]]] =
      maybeProjectRequest(request, projects.withSlug(author, slug))
  }

  def projectAction(pluginId: String)(
      implicit ec: ExecutionContext,
      cs: ContextShift[IO]
  ): ActionRefiner[OreRequest, ProjectRequest] = new ActionRefiner[OreRequest, ProjectRequest] {
    def executionContext: ExecutionContext = ec

    def refine[A](request: OreRequest[A]): Future[Either[Result, ProjectRequest[A]]] =
      maybeProjectRequest(request, projects.withPluginId(pluginId))
  }

  private def maybeProjectRequest[A](r: OreRequest[A], project: OptionT[IO, Model[Project]])(
      implicit cs: ContextShift[IO]
  ): Future[Either[Result, ProjectRequest[A]]] = {
    implicit val request: OreRequest[A] = r
    project
      .flatMap(processProject(_, request.headerData.currentUser))
      .semiflatMap { p =>
        toProjectRequest(p) {
          case (data, scoped) => new ProjectRequest[A](data, scoped, r.headerData, r)
        }
      }
      .toRight(notFound)
      .value
      .unsafeToFuture()
  }

  private def toProjectRequest[T](project: Model[Project])(f: (ProjectData, ScopedProjectData) => T)(
      implicit
      request: OreRequest[_],
      cs: ContextShift[IO]
  ) =
    (ProjectData.of(project), ScopedProjectData.of(request.headerData.currentUser, project)).parMapN(f)

  private def processProject(project: Model[Project], user: Option[Model[User]])(
      implicit cs: ContextShift[IO]
  ): OptionT[IO, Model[Project]] = {
    if (project.visibility == Visibility.Public) {
      OptionT.pure[IO](project)
    } else {
      OptionT
        .fromOption[IO](user)
        .semiflatMap { user =>
          val check1 = canEditAndNeedChangeOrApproval(project, user)
          val check2 = user.permissionsIn(GlobalScope).map(_.has(Permission.SeeHidden))

          IOUtils.raceBoolean(check1, check2)
        }
        .subflatMap {
          case true  => Some(project)
          case false => None
        }
    }
  }

  private def canEditAndNeedChangeOrApproval(project: Model[Project], user: Model[User]) =
    project.visibility match {
      case Visibility.New => user.permissionsIn(project).map(_.has(Permission.CreateVersion))
      case Visibility.NeedsApproval | Visibility.NeedsApproval =>
        user.permissionsIn(project).map(_.has(Permission.EditProjectSettings))
      case _ => IO.pure(false)
    }

  def authedProjectActionImpl(project: OptionT[IO, Model[Project]])(
      implicit ec: ExecutionContext,
      cs: ContextShift[IO]
  ): ActionRefiner[AuthRequest, AuthedProjectRequest] = new ActionRefiner[AuthRequest, AuthedProjectRequest] {

    def executionContext: ExecutionContext = ec

    def refine[A](request: AuthRequest[A]): Future[Either[Result, AuthedProjectRequest[A]]] = {
      implicit val r: AuthRequest[A] = request

      project
        .flatMap(processProject(_, Some(request.user)))
        .semiflatMap { p =>
          toProjectRequest(p) {
            case (data, scoped) => new AuthedProjectRequest[A](data, scoped, r.headerData, request)
          }
        }
        .toRight(notFound)
        .value
        .unsafeToFuture()
    }
  }

  def authedProjectAction(author: String, slug: String)(
      implicit ec: ExecutionContext,
      cs: ContextShift[IO]
  ): ActionRefiner[AuthRequest, AuthedProjectRequest] = authedProjectActionImpl(projects.withSlug(author, slug))

  def authedProjectActionById(pluginId: String)(
      implicit ec: ExecutionContext,
      cs: ContextShift[IO]
  ): ActionRefiner[AuthRequest, AuthedProjectRequest] = authedProjectActionImpl(projects.withPluginId(pluginId))

  def organizationAction(organization: String)(
      implicit ec: ExecutionContext,
      cs: ContextShift[IO]
  ): ActionRefiner[OreRequest, OrganizationRequest] = new ActionRefiner[OreRequest, OrganizationRequest] {

    def executionContext: ExecutionContext = ec

    def refine[A](request: OreRequest[A]): Future[Either[Result, OrganizationRequest[A]]] = {
      implicit val r: OreRequest[A] = request
      getOrga(organization)
        .semiflatMap { org =>
          toOrgaRequest(org) {
            case (data, scoped) => new OrganizationRequest[A](data, scoped, r.headerData, request)
          }
        }
        .toRight(notFound)
        .value
        .unsafeToFuture()
    }
  }

  def authedOrganizationAction(organization: String)(
      implicit ec: ExecutionContext,
      cs: ContextShift[IO]
  ): ActionRefiner[AuthRequest, AuthedOrganizationRequest] = new ActionRefiner[AuthRequest, AuthedOrganizationRequest] {
    def executionContext: ExecutionContext = ec

    def refine[A](request: AuthRequest[A]): Future[Either[Result, AuthedOrganizationRequest[A]]] = {
      implicit val r: AuthRequest[A] = request

      getOrga(organization)
        .semiflatMap { org =>
          toOrgaRequest(org) {
            case (data, scoped) => new AuthedOrganizationRequest[A](data, scoped, r.headerData, request)
          }
        }
        .toRight(notFound)
        .value
        .unsafeToFuture()
    }

  }

  private def toOrgaRequest[T](orga: Model[Organization])(f: (OrganizationData, ScopedOrganizationData) => T)(
      implicit request: OreRequest[_],
      cs: ContextShift[IO]
  ) = (OrganizationData.of(orga), ScopedOrganizationData.of(request.headerData.currentUser, orga)).parMapN(f)

  def getOrga(organization: String): OptionT[IO, Model[Organization]] =
    organizations.withName(organization)

  def getUserData(request: OreRequest[_], userName: String)(implicit cs: ContextShift[IO]): OptionT[IO, UserData] =
    users.withName(userName)(request).semiflatMap(UserData.of(request, _))

}