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

next.plugins.context.scala Maven / Gradle / Ivy

package otoroshi.next.plugins

import otoroshi.env.Env
import otoroshi.gateway.Errors
import otoroshi.next.plugins.api.{
  NgAccess,
  NgAccessContext,
  NgAccessValidator,
  NgPluginCategory,
  NgPluginConfig,
  NgPluginVisibility,
  NgStep
}
import otoroshi.utils.{JsonPathValidator, RegexPool}
import otoroshi.utils.syntax.implicits._
import play.api.libs.json._
import play.api.mvc.Results

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

case class ContextValidationConfig(validators: Seq[JsonPathValidator] = Seq.empty) extends NgPluginConfig {
  def json: JsValue = ContextValidationConfig.format.writes(this)
}

object ContextValidationConfig {
  val format = new Format[ContextValidationConfig] {
    override def writes(o: ContextValidationConfig): JsValue             = Json.obj(
      "validators" -> JsArray(o.validators.map(_.json))
    )
    override def reads(json: JsValue): JsResult[ContextValidationConfig] = Try {
      ContextValidationConfig(
        validators = (json \ "validators")
          .asOpt[Seq[JsValue]]
          .map(_.flatMap(v => JsonPathValidator.format.reads(v).asOpt))
          .getOrElse(Seq.empty)
      )
    } match {
      case Failure(exception) => JsError(exception.getMessage)
      case Success(value)     => JsSuccess(value)
    }
  }
}

class ContextValidation extends NgAccessValidator {

  override def steps: Seq[NgStep]                = Seq(NgStep.ValidateAccess)
  override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.AccessControl)
  override def visibility: NgPluginVisibility    = NgPluginVisibility.NgUserLand

  override def multiInstance: Boolean                      = true
  override def core: Boolean                               = true
  override def name: String                                = "Context validator"
  //override def description: Option[String]                 = "This plugin validates the current context using JSONPath validators".some
  //override def documentation: Option[String]                 =
  override def description: Option[String]                 =
    """This plugin validates the current context using JSONPath validators.
      |
      |This plugin let you configure a list of validators that will check if the current call can pass.
      |A validator is composed of a [JSONPath](https://goessner.net/articles/JsonPath/) that will tell what to check and a value that is the expected value.
      |The JSONPath will be applied on a document that will look like
      |
      |```js
      |{
      |  "snowflake" : "1516772930422308903",
      |  "apikey" : { // current apikey
      |    "clientId" : "vrmElDerycXrofar",
      |    "clientName" : "default-apikey",
      |    "metadata" : {
      |      "foo" : "bar"
      |    },
      |    "tags" : [ ]
      |  },
      |  "user" : null, //  current user
      |  "request" : {
      |    "id" : 1,
      |    "method" : "GET",
      |    "headers" : {
      |      "Host" : "ctx-validation-next-gen.oto.tools:9999",
      |      "Accept" : "*/*",
      |      "User-Agent" : "curl/7.64.1",
      |      "Authorization" : "Basic dnJtRWxEZXJ5Y1hyb2ZhcjpvdDdOSTkyVGI2Q2J4bWVMYU9UNzJxamdCU2JlRHNLbkxtY1FBcXBjVjZTejh0Z3I1b2RUOHAzYjB5SEVNRzhZ",
      |      "Remote-Address" : "127.0.0.1:58929",
      |      "Timeout-Access" : "",
      |      "Raw-Request-URI" : "/foo",
      |      "Tls-Session-Info" : "Session(1650461821330|SSL_NULL_WITH_NULL_NULL)"
      |    },
      |    "cookies" : [ ],
      |    "tls" : false,
      |    "uri" : "/foo",
      |    "path" : "/foo",
      |    "version" : "HTTP/1.1",
      |    "has_body" : false,
      |    "remote" : "127.0.0.1",
      |    "client_cert_chain" : null
      |  },
      |  "config" : {
      |    "validators" : [ {
      |      "path" : "$.apikey.metadata.foo",
      |      "value" : "bar"
      |    } ]
      |  },
      |  "global_config" : { ... }, // global config
      |  "attrs" : {
      |    "otoroshi.core.SnowFlake" : "1516772930422308903",
      |    "otoroshi.core.ElCtx" : {
      |      "requestId" : "1516772930422308903",
      |      "requestSnowflake" : "1516772930422308903",
      |      "requestTimestamp" : "2022-04-20T15:37:01.548+02:00"
      |    },
      |    "otoroshi.next.core.Report" : "otoroshi.next.proxy.NgExecutionReport@277b44e2",
      |    "otoroshi.core.RequestStart" : 1650461821545,
      |    "otoroshi.core.RequestWebsocket" : false,
      |    "otoroshi.core.RequestCounterOut" : 0,
      |    "otoroshi.core.RemainingQuotas" : {
      |      "authorizedCallsPerSec" : 10000000,
      |      "currentCallsPerSec" : 0,
      |      "remainingCallsPerSec" : 10000000,
      |      "authorizedCallsPerDay" : 10000000,
      |      "currentCallsPerDay" : 2,
      |      "remainingCallsPerDay" : 9999998,
      |      "authorizedCallsPerMonth" : 10000000,
      |      "currentCallsPerMonth" : 269,
      |      "remainingCallsPerMonth" : 9999731
      |    },
      |    "otoroshi.next.core.MatchedRoutes" : "MutableList(route_022825450-e97d-42ed-8e22-b23342c1c7c8)",
      |    "otoroshi.core.RequestNumber" : 1,
      |    "otoroshi.next.core.Route" : { ... }, // current route as json
      |    "otoroshi.core.RequestTimestamp" : "2022-04-20T15:37:01.548+02:00",
      |    "otoroshi.core.ApiKey" : { ... }, // current apikey as json
      |    "otoroshi.core.User" : { ... }, // current user as json
      |    "otoroshi.core.RequestCounterIn" : 0
      |  },
      |  "route" : { ... },
      |  "token" : null // current valid jwt token if one
      |}
      |```
      |
      |the expected value support some syntax tricks like
      |
      |* `Not(value)` on a string to check if the current value does not equals another value
      |* `Regex(regex)` on a string to check if the current value matches the regex
      |* `RegexNot(regex)` on a string to check if the current value does not matches the regex
      |* `Wildcard(*value*)` on a string to check if the current value matches the value with wildcards
      |* `WildcardNot(*value*)` on a string to check if the current value does not matches the value with wildcards
      |* `Contains(value)` on a string to check if the current value contains a value
      |* `ContainsNot(value)` on a string to check if the current value does not contains a value
      |* `Contains(Regex(regex))` on an array to check if one of the item of the array matches the regex
      |* `ContainsNot(Regex(regex))` on an array to check if one of the item of the array does not matches the regex
      |* `Contains(Wildcard(*value*))` on an array to check if one of the item of the array matches the wildcard value
      |* `ContainsNot(Wildcard(*value*))` on an array to check if one of the item of the array does not matches the wildcard value
      |* `Contains(value)` on an array to check if the array contains a value
      |* `ContainsNot(value)` on an array to check if the array does not contains a value
      |
      |for instance to check if the current apikey has a metadata name `foo` with a value containing `bar`, you can write the following validator
      |
      |```js
      |{
      |  "path": "$.apikey.metadata.foo",
      |  "value": "Contains(bar)"
      |}
      |```
      |""".stripMargin.some
  override def defaultConfigObject: Option[NgPluginConfig] = ContextValidationConfig().some

  private def validate(ctx: NgAccessContext)(implicit env: Env): Boolean = {
    val config         = ctx.cachedConfig(internalName)(ContextValidationConfig.format).getOrElse(ContextValidationConfig())
    val token: JsValue = ctx.attrs
      .get(otoroshi.next.plugins.Keys.JwtInjectionKey)
      .flatMap(_.decodedToken)
      .map { token =>
        Json.obj(
          "header"  -> token.getHeader.fromBase64.parseJson,
          "payload" -> token.getPayload.fromBase64.parseJson
        )
      }
      .getOrElse(JsNull)
    val json           = ctx.json.asObject ++ Json.obj(
      "route" -> ctx.route.json,
      "token" -> token
    )
    // java.nio.file.Files.writeString(new java.io.File("./ctx.json").toPath, json.prettify.debugPrintln)
    config.validators.forall(validator => validator.validate(json))
  }

  override def access(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): Future[NgAccess] = {
    if (validate(ctx)) {
      NgAccess.NgAllowed.vfuture
    } else {
      Errors
        .craftResponseResult(
          "forbidden",
          Results.Forbidden,
          ctx.request,
          None,
          None,
          duration = ctx.report.getDurationNow(),
          overhead = ctx.report.getOverheadInNow(),
          attrs = ctx.attrs,
          maybeRoute = ctx.route.some
        )
        .map(r => NgAccess.NgDenied(r))
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy