All Downloads are FREE. Search and download functionalities are using the official Maven repository.

actions.api.scala Maven / Gradle / Ivy

package otoroshi.actions

import java.util.Base64
import java.util.concurrent.atomic.AtomicReference
import akka.http.scaladsl.util.FastFuture
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.google.common.base.Charsets
import otoroshi.env.Env
import otoroshi.gateway.Errors
import otoroshi.models.{ApiKey, BackOfficeUser, EntityLocationSupport, _}
import otoroshi.models.RightsChecker.{SuperAdminOnly, TenantAdminOnly}
import otoroshi.utils.syntax.implicits._
import play.api.Logger
import play.api.libs.json.{JsArray, JsValue, Json}
import play.api.mvc._
import otoroshi.security.{IdGenerator, OtoroshiClaim}
import otoroshi.utils.{JsonPathValidator, JsonValidator}
import otoroshi.utils.http.RequestImplicits._

import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}

object ApiActionContext {
  val forbidden  = Results.Forbidden(Json.obj("error" -> "You're not authorized here !"))
  val fforbidden = forbidden.future
}

trait ApiActionContextCapable {

  def apiKey: ApiKey
  def request: RequestHeader

  lazy val forbidden  = ApiActionContext.forbidden
  lazy val fforbidden = ApiActionContext.fforbidden

  def user(implicit env: Env): Option[JsValue] =
    request.headers
      .get(env.Headers.OtoroshiAdminProfile)
      .flatMap(p => Try(Json.parse(new String(Base64.getDecoder.decode(p), Charsets.UTF_8))).toOption)

  def from(implicit env: Env): String = request.theIpAddress

  def ua: String = request.theUserAgent

  lazy val currentTenant: TenantId = {
    TenantId(request.headers.get("Otoroshi-Tenant").getOrElse("default"))
  }

  private val bouRef = new AtomicReference[Either[String, Option[BackOfficeUser]]]()

  def userIsSuperAdmin(implicit env: Env): Boolean = {
    backOfficeUser match {
      case Left(_)           => false
      case Right(None)       => {
        val tenantAccess = apiKey.metadata
          .get("otoroshi-access-rights")
          .map(Json.parse)
          .flatMap(_.asOpt[JsArray])
          .map(UserRights.readFromArray)
        tenantAccess match {
          case None             => true
          case Some(userRights) => userRights.superAdmin
        }
      }
      case Right(Some(user)) => user.rights.superAdmin
    }
  }

  def oneAuthorizedTenant(implicit env: Env): TenantId =
    backOfficeUser.toOption.flatten.map(_.rights.oneAuthorizedTenant).getOrElse(TenantId.default)

  def oneAuthorizedTeam(implicit env: Env): TeamId =
    backOfficeUser.toOption.flatten.map(_.rights.oneAuthorizedTeam).getOrElse(TeamId.default)

  def backOfficeUser(implicit env: Env): Either[String, Option[BackOfficeUser]] = {
    Option(bouRef.get()).getOrElse {
      (
        if (env.bypassUserRightsCheck) {
          Right(None)
        } else {
          request.headers.get("Otoroshi-BackOffice-User") match {
            case None          =>
              val tenantAccess = apiKey.metadata
                .get("otoroshi-access-rights")
                .map(Json.parse)
                .flatMap(_.asOpt[JsArray])
                .map(UserRights.readFromArray)
              tenantAccess match {
                case None             => Right(None)
                case Some(userRights) =>
                  val user = BackOfficeUser(
                    randomId = IdGenerator.token,
                    name = apiKey.clientName,
                    email = apiKey.clientId,
                    profile = Json.obj(),
                    authConfigId = "apikey",
                    simpleLogin = false,
                    tags = Seq.empty,
                    metadata = Map.empty,
                    rights = userRights,
                    location = EntityLocation(currentTenant, teams = Seq(TeamId.all)),
                    adminEntityValidators = Map.empty
                  )
                  Right(user.some)
                case _                => Left("You're not authorized here (invalid setup) ! ")
              }
            case Some(userJwt) =>
              Try(JWT.require(Algorithm.HMAC512(apiKey.clientSecret)).build().verify(userJwt)) match {
                case Failure(e)       =>
                  Left("You're not authorized here !")
                case Success(decoded) => {
                  Option(decoded.getClaim("user"))
                    .flatMap(c => Try(c.asString()).toOption)
                    .flatMap(u => Try(Json.parse(u)).toOption)
                    .flatMap(u => BackOfficeUser.fmt.reads(u).asOpt) match {
                    case None       => Left("You're not authorized here !")
                    case Some(user) => Right(user.some)
                  }
                }
              }
          }
        }
      ).debug { either =>
        bouRef.set(either)
      }
    }
  }

  private def rootOrTenantAdmin(user: BackOfficeUser)(f: => Boolean)(implicit env: Env): Boolean = {
    if (env.bypassUserRightsCheck || SuperAdminOnly.canPerform(user, currentTenant)) { // || TenantAdminOnly.canPerform(user, currentTenant)) {
      true
    } else {
      f
    }
  }

  def canUserReadJson(item: JsValue)(implicit env: Env): Boolean = {
    canUserRead(FakeEntityLocationSupport(EntityLocation.readFromKey(item)))
  }

  def canUserRead[T <: EntityLocationSupport](item: T)(implicit env: Env): Boolean = {
    backOfficeUser match {
      case Left(_)           => false
      case Right(None)       => true
      case Right(Some(user)) =>
        rootOrTenantAdmin(user) {
          item match {
            case _: Tenant =>
              user.rights.canReadTenant(item.location.tenant)
            case _         =>
              (currentTenant.value == item.location.tenant.value || item.location.tenant == TenantId.all) && user.rights
                .canReadTenant(item.location.tenant) && user.rights.canReadTeams(currentTenant, item.location.teams)
          }
        }
    }
  }

  def canUserWriteJson(item: JsValue)(implicit env: Env): Boolean = {
    canUserWrite(FakeEntityLocationSupport(EntityLocation.readFromKey(item)))
  }

  def canUserWrite[T <: EntityLocationSupport](item: T)(implicit env: Env): Boolean = {
    backOfficeUser match {
      case Left(_)           => false
      case Right(None)       => true
      case Right(Some(user)) =>
        rootOrTenantAdmin(user) {
          item match {
            case _: Tenant =>
              (currentTenant.value == item.location.tenant.value || item.location.tenant == TenantId.all) && user.rights
                .canWriteTenant(item.location.tenant)
            case _         =>
              (currentTenant.value == item.location.tenant.value || item.location.tenant == TenantId.all) && user.rights
                .canWriteTenant(item.location.tenant) && user.rights.canWriteTeams(currentTenant, item.location.teams)
          }
        }
    }
  }

  def checkRights(rc: RightsChecker)(f: Future[Result])(implicit ec: ExecutionContext, env: Env): Future[Result] = {
    if (env.bypassUserRightsCheck) {
      f
    } else {
      backOfficeUser match {
        case Left(error)       => Results.Forbidden(Json.obj("error" -> error)).future
        case Right(None)       => f // standard api usage without limitations
        case Right(Some(user)) =>
          if (rc.canPerform(user, currentTenant)) {
            f
          } else {
            Results.Forbidden(Json.obj("error" -> "You're not authorized here !")).future
          }
      }
    }
  }

  private def findServiceById(
      serviceId: String
  )(implicit ec: ExecutionContext, env: Env): Future[Option[ServiceDescriptor]] = {
    env.datastores.serviceDescriptorDataStore.findById(serviceId) flatMap {
      case Some(service) => service.some.vfuture
      case None          =>
        env.datastores.routeDataStore.findById(serviceId) flatMap {
          case Some(service) => service.legacy.some.vfuture
          case None          =>
            env.datastores.routeCompositionDataStore.findById(serviceId) map {
              case Some(service) => service.toRoutes.head.legacy.some
              case None          => None
            }
        }
    }
  }

  /// utils methods
  def canReadService(id: String)(f: => Future[Result])(implicit ec: ExecutionContext, env: Env): Future[Result] = {
    if (id == "global") {
      f
    } else {
      findServiceById(id).flatMap {
        case Some(service) if canUserRead(service) => f
        case _                                     => fforbidden
      }
    }
  }

  def canWriteService(id: String)(f: => Future[Result])(implicit ec: ExecutionContext, env: Env): Future[Result] = {
    findServiceById(id).flatMap {
      case Some(service) if canUserWrite(service) => f
      case _                                      => fforbidden
    }
  }

  def canReadApikey(id: String)(f: => Future[Result])(implicit ec: ExecutionContext, env: Env): Future[Result] = {
    env.datastores.apiKeyDataStore.findById(id).flatMap {
      case Some(service) if canUserRead(service) => f
      case _                                     => fforbidden
    }
  }

  def canWriteApikey(id: String)(f: => Future[Result])(implicit ec: ExecutionContext, env: Env): Future[Result] = {
    env.datastores.apiKeyDataStore.findById(id).flatMap {
      case Some(service) if canUserWrite(service) => f
      case _                                      => fforbidden
    }
  }

  def canReadGroup(id: String)(f: => Future[Result])(implicit ec: ExecutionContext, env: Env): Future[Result] = {
    env.datastores.serviceGroupDataStore.findById(id).flatMap {
      case Some(service) if canUserRead(service) => f
      case _                                     => fforbidden
    }
  }

  def canWriteGroup(id: String)(f: => Future[Result])(implicit ec: ExecutionContext, env: Env): Future[Result] = {
    env.datastores.serviceGroupDataStore.findById(id).flatMap {
      case Some(service) if canUserWrite(service) => f
      case _                                      => fforbidden
    }
  }

  def canWriteAuthModule(id: String)(f: => Future[Result])(implicit ec: ExecutionContext, env: Env): Future[Result] = {
    // env.datastores.authConfigsDataStore.findById(id).flatMap {
    env.proxyState.authModuleAsync(id).flatMap {
      case Some(mod) if canUserWrite(mod) => f
      case _                              => fforbidden
    }
  }

  def validateEntity(json: JsValue, singularName: String)(implicit env: Env): Either[JsValue, JsValue] = {
    backOfficeUser match {
      case Left(err)         => Left(err.json)
      case Right(None)       => Right(json)
      case Right(Some(user)) => {
        val envValidators: Seq[JsonValidator]  =
          env.adminEntityValidators.getOrElse("all", Seq.empty[JsonValidator]) ++ env.adminEntityValidators
            .getOrElse(singularName.toLowerCase, Seq.empty[JsonValidator])
        val userValidators: Seq[JsonValidator] =
          user.adminEntityValidators.getOrElse("all", Seq.empty[JsonValidator]) ++ user.adminEntityValidators
            .getOrElse(singularName.toLowerCase, Seq.empty[JsonValidator])
        val validators                         = envValidators ++ userValidators
        val failedValidators                   = validators.filterNot(_.validate(json))
        if (failedValidators.isEmpty) {
          Right(json)
        } else {
          val errors = failedValidators.flatMap(_.error).map(_.json)
          if (errors.isEmpty) {
            Left("entity validation failed".json)
          } else {
            Left(JsArray(errors))
          }
        }
      }
    }
  }
}

case class ApiActionContext[A](apiKey: ApiKey, request: Request[A]) extends ApiActionContextCapable {}

class ApiAction(val parser: BodyParser[AnyContent])(implicit env: Env)
    extends ActionBuilder[ApiActionContext, AnyContent]
    with ActionFunction[Request, ApiActionContext] {

  implicit lazy val ec = env.otoroshiExecutionContext

  lazy val logger = Logger("otoroshi-api-action")

  def decodeBase64(encoded: String): String = new String(OtoroshiClaim.decoder.decode(encoded), Charsets.UTF_8)

  def error(message: String, ex: Option[Throwable] = None)(implicit request: Request[_]): Future[Result] = {
    ex match {
      case Some(e) => logger.error(s"error message: $message", e)
      case None    => logger.error(s"error message: $message")
    }
    FastFuture.successful(
      Results
        .Unauthorized(Json.obj("error" -> message))
        .withHeaders(
          env.Headers.OtoroshiStateResp -> request.headers.get(env.Headers.OtoroshiState).getOrElse("--")
        )
    )
  }

  override def invokeBlock[A](request: Request[A], block: ApiActionContext[A] => Future[Result]): Future[Result] = {

    implicit val req = request

    val host = request.theDomain // if (request.host.contains(":")) request.host.split(":")(0) else request.host
    def perform(): Future[Result] = {
      request.headers.get(env.Headers.OtoroshiClaim).get.split("\\.").toSeq match {
        case Seq(head, body, signature) => {
          val claim          = Json.parse(new String(OtoroshiClaim.decoder.decode(body), Charsets.UTF_8))
          val lastestApikey  = (claim \ "access_type").asOpt[String].exists(v => v == "apikey" || v == "both")
          val latestClientId = (claim \ "apikey" \ "clientId").asOpt[String]
          (claim \ "sub").as[String].split(":").toSeq match {
            case Seq("apikey", clientId)                        => {
              env.datastores.globalConfigDataStore
                .singleton()
                .filter(c => request.method.toLowerCase() == "get" || !c.apiReadOnly)
                .flatMap { _ =>
                  env.datastores.apiKeyDataStore.findById(clientId).flatMap {
                    case Some(apikey)
                        if apikey.authorizedOnGroup(env.backOfficeGroup.id) || apikey
                          .authorizedOnService(env.backOfficeDescriptor.id) => {
                      block(ApiActionContext(apikey, request)).foldM {
                        case Success(res) =>
                          res
                            .withHeaders(
                              env.Headers.OtoroshiStateResp -> request.headers
                                .get(env.Headers.OtoroshiState)
                                .getOrElse("--")
                            )
                            .asFuture
                        case Failure(err) => error(s"Server error : $err", Some(err))
                      }
                    }
                    case _ => error(s"You're not authorized - ${request.method} ${request.uri}")
                  }
                } recoverWith { case e =>
                e.printStackTrace()
                error(s"You're not authorized - ${request.method} ${request.uri}")
              }
            }
            case _ if lastestApikey && latestClientId.isDefined => {
              env.datastores.globalConfigDataStore
                .singleton()
                .filter(c => request.method.toLowerCase() == "get" || !c.apiReadOnly)
                .flatMap { _ =>
                  env.datastores.apiKeyDataStore.findById(latestClientId.get).flatMap {
                    case Some(apikey)
                        if apikey.authorizedOnGroup(env.backOfficeGroup.id) || apikey
                          .authorizedOnService(env.backOfficeDescriptor.id) => {
                      block(ApiActionContext(apikey, request)).foldM {
                        case Success(res) =>
                          res
                            .withHeaders(
                              env.Headers.OtoroshiStateResp -> request.headers
                                .get(env.Headers.OtoroshiState)
                                .getOrElse("--")
                            )
                            .asFuture
                        case Failure(err) => error(s"Server error : $err", Some(err))
                      }
                    }
                    case _ => error(s"You're not authorized - ${request.method} ${request.uri}")
                  }
                } recoverWith { case e =>
                e.printStackTrace()
                error(s"You're not authorized - ${request.method} ${request.uri}")
              }
            }
            case _                                              => error(s"You're not authorized - ${request.method} ${request.uri}")
          }
        }
        case _                          => error(s"You're not authorized - ${request.method} ${request.uri}")
      }
    }
    host match {
      case env.adminApiHost                     => perform()
      case h if env.adminApiDomains.contains(h) => perform()
      case _                                    => error(s"Not found")
    }
  }

  override protected def executionContext: ExecutionContext = ec
}

case class UnAuthApiActionContent[A](req: Request[A])

class UnAuthApiAction(val parser: BodyParser[AnyContent])(implicit env: Env)
    extends ActionBuilder[UnAuthApiActionContent, AnyContent]
    with ActionFunction[Request, UnAuthApiActionContent] {

  implicit lazy val ec = env.otoroshiExecutionContext

  lazy val logger = Logger("otoroshi-api-action")

  def error(message: String, ex: Option[Throwable] = None)(implicit request: Request[_]): Future[Result] = {
    ex match {
      case Some(e) => logger.error(s"error message: $message", e)
      case None    => logger.error(s"error message: $message")
    }
    FastFuture.successful(
      Results
        .Unauthorized(Json.obj("error" -> message))
        .withHeaders(
          env.Headers.OtoroshiStateResp -> request.headers.get(env.Headers.OtoroshiState).getOrElse("--")
        )
    )
  }

  override def invokeBlock[A](
      request: Request[A],
      block: UnAuthApiActionContent[A] => Future[Result]
  ): Future[Result] = {

    implicit val req = request

    val host = request.theDomain // if (request.host.contains(":")) request.host.split(":")(0) else request.host
    host match {
      case env.adminApiHost                     => block(UnAuthApiActionContent(request))
      case h if env.adminApiDomains.contains(h) => block(UnAuthApiActionContent(request))
      case _                                    => error(s"Not found")
    }
  }

  override protected def executionContext: ExecutionContext = ec
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy