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

controllers.adminapi.ScriptApiController.scala Maven / Gradle / Ivy

package otoroshi.controllers.adminapi

import otoroshi.actions.ApiAction
import akka.util.ByteString
import otoroshi.env.Env
import otoroshi.models.RightsChecker.Anyone
import otoroshi.next.plugins.WasmJob
import otoroshi.script._
import otoroshi.utils.controllers.{
  ApiError,
  BulkControllerHelper,
  CrudControllerHelper,
  EntityAndContext,
  JsonApiError,
  NoEntityAndContext,
  OptionalEntityAndContext,
  SeqEntityAndContext
}
import play.api.Logger
import play.api.libs.json._
import play.api.libs.streams.Accumulator
import play.api.mvc._
import otoroshi.utils.syntax.implicits._

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

class ScriptApiController(val ApiAction: ApiAction, val cc: ControllerComponents)(implicit val env: Env)
    extends AbstractController(cc)
    with BulkControllerHelper[Script, JsValue]
    with CrudControllerHelper[Script, JsValue] {

  implicit lazy val ec  = env.otoroshiExecutionContext
  implicit lazy val mat = env.otoroshiMaterializer

  val logger = Logger("otoroshi-scripts-api")

  val sourceBodyParser = BodyParser("scripts-parsers") { _ =>
    Accumulator.source[ByteString].map(Right.apply)
  }

  override def singularName: String = "script"

  override def buildError(status: Int, message: String): ApiError[JsValue] =
    JsonApiError(status, play.api.libs.json.JsString(message))

  def OnlyIfScriptingEnabled(f: => Future[Result]): Future[Result] = {
    env.scriptingEnabled match {
      case true  => f
      case false => InternalServerError(Json.obj("error" -> "Scripting not enabled !")).future
    }
  }

  def findAllScriptsList() =
    ApiAction.async { ctx =>
      val transformersNames  = env.scriptManager.transformersNames
      val validatorsNames    = env.scriptManager.validatorsNames
      val preRouteNames      = env.scriptManager.preRouteNames
      val reqSinkNames       = env.scriptManager.reqSinkNames
      val listenerNames      = env.scriptManager.listenerNames
      val jobNames           = env.scriptManager.jobNames
      val exporterNames      = env.scriptManager.exporterNames
      val reqHandlerNames    = env.scriptManager.reqHandlerNames
      val tunnelHandlerNames = env.scriptManager.tunnelHandlerNames

      val typ                        = ctx.request.getQueryString("type")
      val excludedTypes: Seq[String] = ctx.request
        .getQueryString("excluded_types")
        .map(a => a.split(",").toList)
        .getOrElse(Seq.empty[String])

      val cpTransformers  = typ match {
        case None                => transformersNames
        case Some("transformer") => transformersNames
        case Some("app")         => transformersNames
        case _                   => Seq.empty
      }
      val cpValidators    = typ match {
        case None              => validatorsNames
        case Some("validator") => validatorsNames
        case _                 => Seq.empty
      }
      val cpPreRoutes     = typ match {
        case None             => preRouteNames
        case Some("preroute") => preRouteNames
        case _                => Seq.empty
      }
      val cpRequestSinks  = typ match {
        case None         => reqSinkNames
        case Some("sink") => reqSinkNames
        case _            => Seq.empty
      }
      val cpListenerNames = typ match {
        case None             => listenerNames
        case Some("listener") => listenerNames
        case _                => Seq.empty
      }
      val cpJobNames      = typ match {
        case None        => jobNames
        case Some("job") => jobNames
        case _           => Seq.empty
      }
      val cpExporterNames = typ match {
        case None             => exporterNames
        case Some("exporter") => exporterNames
        case _                => Seq.empty
      }
      val reqHandlers     = typ match {
        case None                    => reqHandlerNames
        case Some("request-handler") => reqHandlerNames
        case _                       => Seq.empty
      }
      val tunnelHandlers  = typ match {
        case None                   => tunnelHandlerNames
        case Some("tunnel-handler") => tunnelHandlerNames
        case _                      => Seq.empty
      }
      def extractInfosFromJob(c: String): JsValue = {
        env.scriptManager.getAnyScript[Job](s"cp:$c") match {
          case Left(_)                                                             => extractInfos(c)
          case Right(instance) if instance.jobVisibility == JobVisibility.UserLand => extractInfos(c)
          case Right(instance) if instance.jobVisibility == JobVisibility.Internal => JsNull
        }
      }
      def extractInfos(c: String): JsValue = {
        env.scriptManager.getAnyScript[NamedPlugin](s"cp:$c") match {
          case Left(_)         =>
            Json.obj(
              "id"          -> s"cp:$c",
              "name"        -> c,
              "description" -> JsNull,
              "pluginType"  -> PluginType.CompositeType.name
            )
          case Right(instance) =>
            instance.jsonDescription ++ Json.obj(
              "id"                -> s"cp:$c",
              "name"              -> instance.name,
              "pluginType"        -> instance.pluginType.name,
              "config_schema"     -> instance.configSchema.getOrElse(JsNull).as[JsValue],
              "config_flow"       -> JsArray(instance.configFlow.map(JsString.apply)),
              "plugin_visibility" -> instance.visibility.json,
              "plugin_categories" -> JsArray(instance.categories.map(_.json)),
              "plugin_steps"      -> JsArray(instance.steps.map(_.json))
            )
        }
      }
      env.datastores.scriptDataStore.findAll().map { all =>
        val allClasses = all
          .filter(ctx.canUserRead)
          .filter { script =>
            typ match {
              case None                                                                    => true
              case Some("transformer") if script.`type` == PluginType.TransformerType      => true
              case Some("transformer") if script.`type` == PluginType.AppType              => true
              case Some("app") if script.`type` == PluginType.AppType                      => true
              case Some("validator") if script.`type` == PluginType.AccessValidatorType    => true
              case Some("preroute") if script.`type` == PluginType.PreRoutingType          => true
              case Some("sink") if script.`type` == PluginType.RequestSinkType             => true
              case Some("listener") if script.`type` == PluginType.EventListenerType       => true
              case Some("job") if script.`type` == PluginType.JobType                      => true
              case Some("exporter") if script.`type` == PluginType.DataExporterType        => true
              case Some("request-handler") if script.`type` == PluginType.DataExporterType => true
              case Some("tunnel-handler") if script.`type` == PluginType.TunnelHandlerType => true
              case Some("composite") if script.`type` == PluginType.CompositeType          => true
              case Some("*")                                                               => true
              case _                                                                       => false
            }
          }
          .map(c => (c, env.scriptManager.getAnyScript[NamedPlugin](c.id)))
          .map {
            case (c, Left(_))         =>
              Json.obj(
                "id"          -> c.id,
                "name"        -> c.name,
                "description" -> c.desc,
                "pluginType"  -> PluginType.CompositeType.name
              )
            case (c, Right(instance)) =>
              Json.obj(
                "id"                -> c.id,
                "name"              -> JsString(Option(c.name).map(_.trim).filter(_.nonEmpty).getOrElse(instance.name)),
                "description"       -> Option(c.desc)
                  .map(_.trim)
                  .filter(_.nonEmpty)
                  .orElse(instance.description)
                  .map(JsString.apply)
                  .getOrElse(JsNull)
                  .as[JsValue],
                "pluginType"        -> instance.pluginType.name,
                "defaultConfig"     -> instance.defaultConfig.getOrElse(JsNull).as[JsValue],
                "configRoot"        -> instance.configRoot.map(JsString.apply).getOrElse(JsNull).as[JsValue],
                "configSchema"      -> instance.configSchema.getOrElse(JsNull).as[JsValue],
                "configFlow"        -> JsArray(instance.configFlow.map(JsString.apply)),
                "plugin_type"       -> instance.pluginType.name,
                "default_config"    -> instance.defaultConfig.getOrElse(JsNull).as[JsValue],
                "config_root"       -> instance.configRoot.map(JsString.apply).getOrElse(JsNull).as[JsValue],
                "config_schema"     -> instance.configSchema.getOrElse(JsNull).as[JsValue],
                "config_flow"       -> JsArray(instance.configFlow.map(JsString.apply)),
                "plugin_visibility" -> instance.visibility.json,
                "plugin_categories" -> JsArray(instance.categories.map(_.json)),
                "plugin_steps"      -> JsArray(instance.steps.map(_.json))
              )
          } ++
          cpTransformers.map(extractInfos) ++
          cpValidators.map(extractInfos) ++
          cpPreRoutes.map(extractInfos) ++
          cpRequestSinks.map(extractInfos) ++
          cpListenerNames.map(extractInfos) ++
          cpExporterNames.map(extractInfos) ++
          reqHandlers.map(extractInfos) ++
          tunnelHandlers.map(extractInfos) ++
          cpJobNames.filter(_ != classOf[WasmJob].getName).map(extractInfosFromJob).filter {
            case JsNull => false
            case _      => true
          }
        Ok(JsArray(allClasses))
      }
    }

  def compileScript() =
    ApiAction.async(sourceBodyParser) { ctx =>
      ctx.checkRights(Anyone) {
        OnlyIfScriptingEnabled {
          ctx.request.body.runFold(ByteString.empty)(_ ++ _).flatMap { body =>
            val code = Json.parse(body.utf8String).\("code").as[String]
            env.scriptCompiler.compile(code).map {
              case Left(err) => Ok(Json.obj("done" -> true, "error" -> err))
              case Right(_)  => Ok(Json.obj("done" -> true))
            }
          }
        }
      }
    }

  override def extractId(entity: Script): String = entity.id

  override def readEntity(json: JsValue): Either[JsValue, Script] =
    Script._fmt.reads(json).asEither match {
      case Left(e)  => Left(JsError.toJson(e))
      case Right(r) => Right(r)
    }

  override def writeEntity(entity: Script): JsValue = Script._fmt.writes(entity)

  override def findByIdOps(
      id: String,
      req: RequestHeader
  )(implicit env: Env, ec: ExecutionContext): Future[Either[ApiError[JsValue], OptionalEntityAndContext[Script]]] = {
    env.datastores.scriptDataStore.findById(id).map { opt =>
      Right(
        OptionalEntityAndContext(
          entity = opt,
          action = "ACCESS_SCRIPT",
          message = "User accessed a script",
          metadata = Json.obj("ScriptId" -> id),
          alert = "ScriptAccessed"
        )
      )
    }
  }

  override def findAllOps(
      req: RequestHeader
  )(implicit env: Env, ec: ExecutionContext): Future[Either[ApiError[JsValue], SeqEntityAndContext[Script]]] = {
    env.datastores.scriptDataStore.findAll().map { seq =>
      Right(
        SeqEntityAndContext(
          entity = seq,
          action = "ACCESS_ALL_SCRIPTS",
          message = "User accessed all scripts",
          metadata = Json.obj(),
          alert = "ScriptsAccessed"
        )
      )
    }
  }

  override def createEntityOps(
      entity: Script,
      req: RequestHeader
  )(implicit env: Env, ec: ExecutionContext): Future[Either[ApiError[JsValue], EntityAndContext[Script]]] = {
    env.datastores.scriptDataStore.set(entity).map {
      case true  => {
        Right(
          EntityAndContext(
            entity = entity,
            action = "CREATE_SCRIPT",
            message = "User created a script",
            metadata = entity.toJson.as[JsObject],
            alert = "ScriptCreatedAlert"
          )
        )
      }
      case false => {
        Left(
          JsonApiError(
            500,
            Json.obj("error" -> "Script not stored ...")
          )
        )
      }
    }
  }

  override def updateEntityOps(
      entity: Script,
      req: RequestHeader
  )(implicit env: Env, ec: ExecutionContext): Future[Either[ApiError[JsValue], EntityAndContext[Script]]] = {
    env.datastores.scriptDataStore.set(entity).map {
      case true  => {
        Right(
          EntityAndContext(
            entity = entity,
            action = "UPDATE_SCRIPT",
            message = "User updated a script",
            metadata = entity.toJson.as[JsObject],
            alert = "ScriptUpdatedAlert"
          )
        )
      }
      case false => {
        Left(
          JsonApiError(
            500,
            Json.obj("error" -> "Script not stored ...")
          )
        )
      }
    }
  }

  override def deleteEntityOps(
      id: String,
      req: RequestHeader
  )(implicit env: Env, ec: ExecutionContext): Future[Either[ApiError[JsValue], NoEntityAndContext[Script]]] = {
    env.datastores.scriptDataStore.delete(id).map {
      case true  => {
        Right(
          NoEntityAndContext(
            action = "DELETE_SCRIPT",
            message = "User deleted a Script",
            metadata = Json.obj("ScriptId" -> id),
            alert = "ScriptDeletedAlert"
          )
        )
      }
      case false => {
        Left(
          JsonApiError(
            500,
            Json.obj("error" -> "Script not deleted ...")
          )
        )
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy