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

next.utils.vault.scala Maven / Gradle / Ivy

The newest version!
package otoroshi.next.utils

import akka.Done
import akka.http.scaladsl.model.Uri
import akka.stream.scaladsl.{Sink, Source}
import com.amazonaws.auth.{AWSStaticCredentialsProvider, BasicAWSCredentials}
import com.amazonaws.handlers.AsyncHandler
import com.amazonaws.services.secretsmanager.AWSSecretsManagerAsyncClientBuilder
import com.amazonaws.services.secretsmanager.model.{GetSecretValueRequest, GetSecretValueResult}
import com.github.blemale.scaffeine.Scaffeine
import com.google.common.base.Charsets
import com.nimbusds.jose.jwk.JWK
import org.joda.time.DateTime
import otoroshi.env.Env
import otoroshi.plugins.jobs.kubernetes.{KubernetesClient, KubernetesConfig}
import otoroshi.ssl.SSLImplicits._
import otoroshi.utils.ReplaceAllWith
import otoroshi.utils.cache.Caches
import otoroshi.utils.cache.types.UnboundedTrieMap
import otoroshi.utils.crypto.Signatures
import otoroshi.utils.syntax.implicits._
import play.api.libs.json._
import play.api.libs.ws.WSAuthScheme
import play.api.{Configuration, Logger}

import java.net.URLEncoder
import java.util.Base64
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
import javax.crypto.spec.{GCMParameterSpec, SecretKeySpec}
import javax.crypto.{Cipher, KeyGenerator}
import scala.collection.concurrent.TrieMap
import scala.concurrent.duration.{DurationInt, DurationLong, FiniteDuration}
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.util.{Failure, Success, Try}

sealed trait AzureSecretKind {
  def path: String
  def get(json: JsValue, base64: Boolean): CachedVaultSecretStatus
}

object AzureSecretKind {
  case object AzureSecretCertificate extends AzureSecretKind {
    def path: String = "certificates"
    def get(json: JsValue, base64: Boolean): CachedVaultSecretStatus = {
      CachedVaultSecretStatus.SecretReadSuccess(
        otoroshi.ssl.PemHeaders.BeginCertificate + "\n" +
        json.select("cer").asString.decodeBase64.grouped(64).mkString("\n") +
        otoroshi.ssl.PemHeaders.EndCertificate + "\n"
      )
    }
  }
  case object AzureSecretPrivateKey  extends AzureSecretKind {
    def path: String = "keys"
    def get(json: JsValue, base64: Boolean): CachedVaultSecretStatus = {
      val jwk = JWK.parse(json.select("key").asObject.stringify)
      jwk.getKeyType.getValue match {
        case "EC"  => CachedVaultSecretStatus.SecretReadSuccess(jwk.toECKey.toPrivateKey.encoded)
        case "RSA" => CachedVaultSecretStatus.SecretReadSuccess(jwk.toRSAKey.toPrivateKey.encoded)
        case t     => CachedVaultSecretStatus.SecretReadError(s"bad jwk type: ${t}")
      }
    }
  }
  case object AzureSecretPublicKey   extends AzureSecretKind {
    def path: String = "keys"
    def get(json: JsValue, base64: Boolean): CachedVaultSecretStatus = {
      val jwk = JWK.parse(json.select("key").asObject.stringify)
      jwk.getKeyType.getValue match {
        case "EC"  => CachedVaultSecretStatus.SecretReadSuccess(jwk.toECKey.toPublicKey.encoded)
        case "RSA" => CachedVaultSecretStatus.SecretReadSuccess(jwk.toRSAKey.toPublicKey.encoded)
        case t     => CachedVaultSecretStatus.SecretReadError(s"bad jwk type: ${t}")
      }
    }
  }
  case object AzureSecret            extends AzureSecretKind {
    def path: String = "secrets"
    def get(json: JsValue, base64: Boolean): CachedVaultSecretStatus = {
      json.select("value").asOpt[JsValue] match {
        case Some(JsString(value)) if base64 => CachedVaultSecretStatus.SecretReadSuccess(value.decodeBase64)
        case Some(JsString(value))           => CachedVaultSecretStatus.SecretReadSuccess(value)
        case Some(JsNumber(value))           => CachedVaultSecretStatus.SecretReadSuccess(value.toString())
        case Some(JsBoolean(value))          => CachedVaultSecretStatus.SecretReadSuccess(value.toString)
        case Some(o @ JsObject(_))           => CachedVaultSecretStatus.SecretReadSuccess(o.stringify)
        case Some(arr @ JsArray(_))          => CachedVaultSecretStatus.SecretReadSuccess(arr.stringify)
        case Some(JsNull)                    => CachedVaultSecretStatus.SecretReadSuccess("null")
        case _                               => CachedVaultSecretStatus.SecretValueNotFound
      }
    }
  }
}

sealed trait CachedVaultSecretStatus {
  def value: String
}

object CachedVaultSecretStatus {
  case object VaultNotFound                    extends CachedVaultSecretStatus { def value: String = "vault-not-found"        }
  case object BadSecretPath                    extends CachedVaultSecretStatus { def value: String = "bad-secret-path"        }
  case object SecretNotFound                   extends CachedVaultSecretStatus { def value: String = "secret-not-found"       }
  case object SecretValueNotFound              extends CachedVaultSecretStatus { def value: String = "secret-value-not-found" }
  case object SecretReadUnauthorized           extends CachedVaultSecretStatus {
    def value: String = "secret-read-not-authorized"
  }
  case object SecretReadForbidden              extends CachedVaultSecretStatus { def value: String = "secret-read-forbidden"  }
  case object SecretReadTimeout                extends CachedVaultSecretStatus { def value: String = "secret-read-timeout"    }
  case class SecretReadError(error: String)    extends CachedVaultSecretStatus {
    def value: String = s"secret-read-error: ${error}"
  }
  case class SecretReadSuccess(secret: String) extends CachedVaultSecretStatus { def value: String = secret                   }
}

case class CachedVaultSecret(key: String, at: DateTime, status: CachedVaultSecretStatus)

trait Vault {
  def get(path: String, options: Map[String, String])(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[CachedVaultSecretStatus]
}

class EnvVault(vaultName: String, configuration: Configuration, _env: Env) extends Vault {

  private val logger        = Logger("otoroshi-env-vault")
  private val defaultPrefix = configuration.getOptionalWithFileSupport[String](s"prefix")
  //env.configuration.getOptionalWithFileSupport[String](s"otoroshi.vaults.${vaultName}.prefix")

  override def get(path: String, options: Map[String, String])(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[CachedVaultSecretStatus] = {
    val prefix = options.get("prefix").orElse(defaultPrefix).getOrElse("")
    val parts  = path.split("/").toSeq.map(_.trim).filterNot(_.isEmpty)
    if (parts.isEmpty) {
      CachedVaultSecretStatus.BadSecretPath.vfuture
    } else if (parts.size == 1) {
      val name = prefix + parts.head
      sys.env.get(name).orElse(sys.env.get(name.toUpperCase())) match {
        case None         => CachedVaultSecretStatus.SecretNotFound.vfuture
        case Some(secret) => CachedVaultSecretStatus.SecretReadSuccess(secret).vfuture
      }
    } else {
      val name    = prefix + parts.head
      val pointer = parts.tail.mkString("/", "/", "")
      sys.env.get(name).orElse(sys.env.get(name.toUpperCase())).filter(_.trim.startsWith("{")).flatMap { jsonraw =>
        Try {
          val obj = Json.parse(jsonraw).asOpt[JsObject].getOrElse(Json.obj())
          obj.atPointer(pointer).asOpt[JsValue] match {
            case Some(JsString(value))  => value.some
            case Some(JsNumber(value))  => value.toString().some
            case Some(JsBoolean(value)) => value.toString.some
            case Some(o @ JsObject(_))  => o.stringify.some
            case Some(arr @ JsArray(_)) => arr.stringify.some
            case Some(JsNull)           => "null".some
            case _                      => None
          }
        } match {
          case Failure(e)            =>
            logger.error("error while trying to read JSON env. variable", e)
            CachedVaultSecretStatus.SecretReadError(e.getMessage).some
          case Success(None)         => CachedVaultSecretStatus.SecretNotFound.some
          case Success(Some(secret)) => CachedVaultSecretStatus.SecretReadSuccess(secret).some
        }
      } match {
        case None         => CachedVaultSecretStatus.SecretNotFound.vfuture
        case Some(status) => status.vfuture
      }
    }
  }
}

class LocalVault(vaultName: String, configuration: Configuration, _env: Env) extends Vault {

  private val logger      = Logger("otoroshi-local-vault")
  private val defaultRoot = configuration.getOptionalWithFileSupport[String](s"root")

  private def extractValue(obj: JsObject, path: String): CachedVaultSecretStatus = {
    Try {
      obj.atPointer(path).asOpt[JsValue] match {
        case Some(JsString(value))  => value.some
        case Some(JsNumber(value))  => value.toString().some
        case Some(JsBoolean(value)) => value.toString.some
        case Some(o @ JsObject(_))  => o.stringify.some
        case Some(arr @ JsArray(_)) => arr.stringify.some
        case Some(JsNull)           => "null".some
        case _                      => None
      }
    } match {
      case Failure(e)            =>
        logger.error("error while trying to read JSON env. variable", e)
        CachedVaultSecretStatus.SecretReadError(e.getMessage)
      case Success(None)         => CachedVaultSecretStatus.SecretNotFound
      case Success(Some(secret)) => CachedVaultSecretStatus.SecretReadSuccess(secret)
    }
  }

  override def get(path: String, options: Map[String, String])(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[CachedVaultSecretStatus] = {
    val parts = path.split("/").toSeq.map(_.trim).filterNot(_.isEmpty)
    if (parts.isEmpty) {
      CachedVaultSecretStatus.BadSecretPath.vfuture
    } else {
      val root = options.get("root").orElse(defaultRoot)
      val name = (root ++ parts).mkString("/", "/", "")
      extractValue(env.datastores.globalConfigDataStore.latest().env, name).vfuture
    }
  }
}

class HashicorpVault(name: String, configuration: Configuration, _env: Env) extends Vault {

  private val logger = Logger("otoroshi-hashicorp-vault")

  private val url   = configuration.getOptionalWithFileSupport[String]("url").getOrElse("http://127.0.0.1:8200")
  //env.configuration.getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.url").getOrElse("http://127.0.0.1:8200")
  private val mount = configuration.getOptionalWithFileSupport[String]("mount").getOrElse("secret")
  //env.configuration.getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.mount").getOrElse("secret")
  private val kv    = configuration.getOptionalWithFileSupport[String](s"kv").getOrElse("v2")
  //env.configuration.getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.kv").getOrElse("v2")
  private val token = configuration.getOptionalWithFileSupport[String](s"token").getOrElse("root")
  //env.configuration.getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.token").getOrElse("root")

  private val baseUrl = s"${url}/v1/${mount}"

  private def dataUrlV2(path: String, options: Map[String, String]) = {
    val opts = if (options.nonEmpty) "?" + options.toSeq.map(v => s"${v._1}=${v._2}").mkString("&") else ""
    s"${baseUrl}/data${path}${opts}"
  }

  private def dataUrlV1(path: String, options: Map[String, String]) = {
    val opts = if (options.nonEmpty) "?" + options.toSeq.map(v => s"${v._1}=${v._2}").mkString("&") else ""
    s"${baseUrl}${path}${opts}"
  }

  override def get(rawpath: String, options: Map[String, String])(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[CachedVaultSecretStatus] = {
    val parts     = rawpath.split("/").toSeq.filterNot(_.isEmpty)
    val path      = parts.init.mkString("/", "/", "")
    val valuename = parts.last
    val url       = if (kv == "v2") dataUrlV2(path, options) else dataUrlV1(path, options)
    env.Ws
      .url(url)
      .withHttpHeaders("X-Vault-Token" -> token)
      .withRequestTimeout(1.minute)
      .withFollowRedirects(false)
      .get()
      .map { response =>
        if (response.status == 200) {
          if (kv == "v2") {
            response.json.select("data").select("data").select(valuename).asOpt[JsValue] match {
              case Some(JsString(value))  => CachedVaultSecretStatus.SecretReadSuccess(value)
              case Some(JsNumber(value))  => CachedVaultSecretStatus.SecretReadSuccess(value.toString())
              case Some(JsBoolean(value)) => CachedVaultSecretStatus.SecretReadSuccess(value.toString)
              case Some(o @ JsObject(_))  => CachedVaultSecretStatus.SecretReadSuccess(o.stringify)
              case Some(arr @ JsArray(_)) => CachedVaultSecretStatus.SecretReadSuccess(arr.stringify)
              case Some(JsNull)           => CachedVaultSecretStatus.SecretReadSuccess("null")
              case _                      => CachedVaultSecretStatus.SecretValueNotFound
            }
          } else {
            response.json.select("data").select(valuename).asOpt[JsValue] match {
              case Some(JsString(value))  => CachedVaultSecretStatus.SecretReadSuccess(value)
              case Some(JsNumber(value))  => CachedVaultSecretStatus.SecretReadSuccess(value.toString())
              case Some(JsBoolean(value)) => CachedVaultSecretStatus.SecretReadSuccess(value.toString)
              case Some(o @ JsObject(_))  => CachedVaultSecretStatus.SecretReadSuccess(o.stringify)
              case Some(arr @ JsArray(_)) => CachedVaultSecretStatus.SecretReadSuccess(arr.stringify)
              case Some(JsNull)           => CachedVaultSecretStatus.SecretReadSuccess("null")
              case _                      => CachedVaultSecretStatus.SecretValueNotFound
            }
          }
        } else if (response.status == 401) {
          CachedVaultSecretStatus.SecretReadUnauthorized
        } else if (response.status == 403) {
          CachedVaultSecretStatus.SecretReadForbidden
        } else {
          CachedVaultSecretStatus.SecretReadError(response.status + " - " + response.body)
        }
      }
      .recover { case e: Throwable =>
        CachedVaultSecretStatus.SecretReadError(e.getMessage)
      }
  }
}

class AzureVault(_name: String, configuration: Configuration, _env: Env) extends Vault {

  private val logger = Logger("otoroshi-azure-vault")

  private val baseUrl                    = configuration
    .getOptionalWithFileSupport[String]("url")
    .getOrElse("https://myvault.vault.azure.net")
  private val apiVersion                 = configuration.getOptionalWithFileSupport[String]("api-version").getOrElse("7.2")
  private val maybetoken: Option[String] = configuration
    .getOptionalWithFileSupport[String]("token")
  private val tokenKey                   = "token"

  private val tokenCache = Scaffeine().maximumSize(2).expireAfterWrite(1.hour).build[String, String]()

  private def dataUrl(path: String, kind: AzureSecretKind, options: Map[String, String]) = {
    val opts =
      if (options.nonEmpty) s"?api-version=${apiVersion}&" + options.toSeq.map(v => s"${v._1}=${v._2}").mkString("&")
      else s"?api-version=${apiVersion}"
    s"${baseUrl}/${kind.path}${path}${opts}"
  }

  private def getToken(): Future[Either[String, String]] = {
    maybetoken match {
      case Some(token) => token.right[String].future
      case None        => {
        tokenCache.getIfPresent(tokenKey) match {
          case Some(token) => token.right[String].future
          case None        => {
            implicit val ec  = _env.otoroshiExecutionContext
            val tenant       = configuration.getOptionalWithFileSupport[String](s"tenant").get
            //env.configuration.getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.tenant").get
            val clientId     =
              configuration.getOptionalWithFileSupport[String](s"client_id").get
            // env.configuration.getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.client_id").get
            val clientSecret =
              configuration.getOptionalWithFileSupport[String](s"client_secret").get
            // env.configuration.getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.client_secret").get
            val url          = s"https://login.microsoftonline.com/${tenant}/oauth2/token"
            if (logger.isDebugEnabled) logger.debug(s"fetching azure access_token from '${url}' ...")
            _env.Ws
              .url(url)
              .post(
                Map(
                  "grant_type"    -> "client_credentials",
                  "client_id"     -> clientId,
                  "client_secret" -> clientSecret,
                  "resource"      -> "https://vault.azure.net"
                )
              )
              .map { resp =>
                if (resp.status == 200) {
                  resp.json.select("access_token").asOpt[String] match {
                    case None              => {
                      tokenCache.invalidate(tokenKey)
                      Left(s"no access_token found in response: ${resp.body}")
                    }
                    case Some(accessToken) => {
                      tokenCache.put(tokenKey, accessToken)
                      accessToken.right[String]
                    }
                  }
                } else {
                  tokenCache.invalidate(tokenKey)
                  Left(s"bad status code for response: ${resp.body}")
                }
              }
              .recover { case e: Throwable =>
                logger.error("error while fetching azure key vault token", e)
                tokenCache.invalidate(tokenKey)
                Left(s"error while fetching azure key vault token: ${e.getMessage}")
              }
          }
        }
      }
    }
  }

  def fetchSecret(url: String, token: String, base64: Boolean, kind: AzureSecretKind)(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[CachedVaultSecretStatus] = {
    if (logger.isDebugEnabled) logger.debug(s"fetching secret at '${url}'")
    env.Ws
      .url(url)
      .withHttpHeaders("Authorization" -> s"Bearer ${token}")
      .withRequestTimeout(1.minute)
      .withFollowRedirects(false)
      .get()
      .map { response =>
        if (response.status == 200) {
          if (logger.isDebugEnabled)
            logger.debug(s"found secret at '${url}'") // with value '${response.json.select("value").asOpt[JsValue]}'")
          kind.get(response.json, base64)
        } else if (response.status == 401) {
          if (logger.isDebugEnabled) logger.debug(s"secret at '$url' not found because of 401: ${response.body}")
          tokenCache.invalidate(tokenKey)
          CachedVaultSecretStatus.SecretReadUnauthorized
        } else if (response.status == 403) {
          if (logger.isDebugEnabled) logger.debug(s"secret at '$url' not found because of 403: ${response.body}")
          // tokenCache.invalidate(tokenKey) ???
          CachedVaultSecretStatus.SecretReadForbidden
        } else {
          CachedVaultSecretStatus.SecretReadError(response.status + " - " + response.body)
        }
      }
      .recover { case e: Throwable =>
        CachedVaultSecretStatus.SecretReadError(e.getMessage)
      }
  }

  override def get(path: String, options: Map[String, String])(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[CachedVaultSecretStatus] = {
    val finalOpts             = options - "azure_secret_base_64" - "azure_secret_kind"
    val base64                = options.get("azure_secret_base_64").contains("true")
    val kind: AzureSecretKind = options.get("azure_secret_kind") match {
      case Some("privkey")     => AzureSecretKind.AzureSecretPrivateKey
      case Some("pubkey")      => AzureSecretKind.AzureSecretPublicKey
      case Some("certificate") => AzureSecretKind.AzureSecretCertificate
      case _                   => AzureSecretKind.AzureSecret
    }
    val url                   = dataUrl(path, kind, finalOpts)
    for {
      token  <- getToken()
      status <- token match {
                  case Left(err)          =>
                    if (logger.isDebugEnabled) logger.debug(s"unable to get access_token: ${err}")
                    CachedVaultSecretStatus.SecretReadError(s"unable to get access_token: ${err}").future
                  case Right(accessToken) => fetchSecret(url, accessToken, base64, kind)
                }
    } yield {
      status
    }
  }
}

class GoogleSecretManagerVault(name: String, configuration: Configuration, _env: Env) extends Vault {

  private val logger  = Logger("otoroshi-gcloud-vault")
  private val baseUrl = configuration
    .getOptionalWithFileSupport[String](s"url")
    .getOrElse("https://secretmanager.googleapis.com")
  // env.configuration
  // .getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.url")
  // .getOrElse("https://secretmanager.googleapis.com")
  private val apikey  =
    configuration.getOptionalWithFileSupport[String](s"apikey").getOrElse("secret")
  // env.configuration.getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.apikey").getOrElse("secret")

  private def dataUrl(path: String, options: Map[String, String]) = {
    val opts =
      if (options.nonEmpty) s"?key=${apikey}&" + options.toSeq.map(v => s"${v._1}=${v._2}").mkString("&")
      else s"?key=${apikey}"
    s"${baseUrl}/v1${path}:access${opts}"
  }

  override def get(path: String, options: Map[String, String])(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[CachedVaultSecretStatus] = {
    val url = dataUrl(path, options)
    env.Ws
      .url(url)
      .withRequestTimeout(1.minute)
      .withFollowRedirects(false)
      .get()
      .map { response =>
        if (response.status == 200) {
          response.json.select("payload").select("data").asOpt[String] match {
            case Some(value) => CachedVaultSecretStatus.SecretReadSuccess(value.fromBase64)
            case _           => CachedVaultSecretStatus.SecretValueNotFound
          }
        } else if (response.status == 401) {
          CachedVaultSecretStatus.SecretReadUnauthorized
        } else if (response.status == 403) {
          CachedVaultSecretStatus.SecretReadForbidden
        } else {
          CachedVaultSecretStatus.SecretReadError(response.status + " - " + response.body)
        }
      }
      .recover { case e: Throwable =>
        CachedVaultSecretStatus.SecretReadError(e.getMessage)
      }
  }
}

class AlibabaCloudSecretManagerVault(name: String, configuration: Configuration, _env: Env) extends Vault {

  private val logger          = Logger("otoroshi-alibaba-cloud-vault")
  private val baseUrl         = configuration
    .getOptionalWithFileSupport[String](s"url")
    .getOrElse("https://kms.eu-central-1.aliyuncs.com")
  // env.configuration
  // .getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.url")
  // .getOrElse("https://kms.eu-central-1.aliyuncs.com")
  private val accessKeyId     = configuration
    .getOptionalWithFileSupport[String](s"access-key-id")
    .getOrElse("access-key")
  // env.configuration
  // .getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.access-key-id")
  // .getOrElse("access-key")
  private val accessKeySecret = configuration
    .getOptionalWithFileSupport[String](s"access-key-secret")
    .getOrElse("secret")
  // env.configuration
  // .getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.access-key-secret")
  // .getOrElse("secret")

  def makeStringToSign(opts: String): String = {
    "GET%2F&" + URLEncoder.encode(opts, Charsets.UTF_8)
  }

  def makeSignature(stringToSign: String, secret: String): String = {
    Base64.getEncoder.encodeToString(Signatures.hmac("HmacSHA1", stringToSign, secret))
  }

  private def dataUrl(path: String, options: Map[String, String]): String = {
    val opts      = if (options.nonEmpty) options.toSeq.map(v => s"${v._1}=${v._2}").mkString("&") else s""
    val name      = path.split("/").filterNot(_.isEmpty).head
    val timestamp = DateTime.now().toString()
    val query     =
      s"Action=GetSecretValue&SecretName=${name}&Format=json&AccessKeyId=${accessKeyId}&SignatureMethod=HMAC-SHA1&Timestamp=${timestamp}&SignatureVersion=1.0&${opts}"
    val signature = makeSignature(query, accessKeySecret)
    s"${baseUrl}/?${query}&Signature=${signature}"
  }

  override def get(path: String, options: Map[String, String])(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[CachedVaultSecretStatus] = {
    val url = dataUrl(path, options)
    env.Ws
      .url(url)
      .withRequestTimeout(1.minute)
      .withFollowRedirects(false)
      .get()
      .map { response =>
        if (response.status == 200) {
          response.json.select("SecretData").asOpt[String] match {
            case Some(value) => CachedVaultSecretStatus.SecretReadSuccess(value)
            case _           => CachedVaultSecretStatus.SecretValueNotFound
          }
        } else if (response.status == 401) {
          CachedVaultSecretStatus.SecretReadUnauthorized
        } else if (response.status == 403) {
          CachedVaultSecretStatus.SecretReadForbidden
        } else {
          CachedVaultSecretStatus.SecretReadError(response.status + " - " + response.body)
        }
      }
      .recover { case e: Throwable =>
        CachedVaultSecretStatus.SecretReadError(e.getMessage)
      }
  }
}

class KubernetesVault(name: String, configuration: Configuration, env: Env) extends Vault {

  private val logger        = Logger("otoroshi-kubernetes-vault")
  private implicit val _env = env
  private implicit val ec   = env.otoroshiExecutionContext

  private val kubeConfig = {
    //env.configurationJson
    configuration.json.select(s"otoroshi").select("vaults").select(name).asOpt[JsValue] match {
      case Some(JsString("global")) => {
        val global = env.datastores.globalConfigDataStore.latest()
        val c1     = global.scripts.jobConfig.select("KubernetesConfig").asOpt[JsObject]
        val c2     = global.plugins.config.select("KubernetesConfig").asOpt[JsObject]
        val c3     = c1.orElse(c2).getOrElse(Json.obj())
        KubernetesConfig.theConfig(c3)
      }
      case Some(obj @ JsObject(_))  => KubernetesConfig.theConfig(obj)
      case _                        => KubernetesConfig.theConfig(KubernetesConfig.defaultConfig)
    }
  }
  private val client = new KubernetesClient(kubeConfig, env)

  override def get(path: String, options: Map[String, String])(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[CachedVaultSecretStatus] = {
    val parts      = path.split("/").toSeq.filterNot(_.isEmpty)
    val namespace  = parts.head
    val secretName = parts.tail.head
    client
      .fetchSecret(namespace, secretName)
      .map {
        case None         => CachedVaultSecretStatus.SecretNotFound
        case Some(secret) => {
          if (parts.size > 2 && secret.hasStringData) {
            val valueName = parts.tail.tail.head
            secret.stringData.getOrElse(Map.empty).get(valueName) match {
              case None        => CachedVaultSecretStatus.SecretValueNotFound
              case Some(value) => CachedVaultSecretStatus.SecretReadSuccess(value)
            }
          } else if (parts.size > 2) {
            CachedVaultSecretStatus.SecretValueNotFound
          } else {
            CachedVaultSecretStatus.SecretReadSuccess(secret.data)
          }
        }
      }
      .recover { case e: Throwable =>
        CachedVaultSecretStatus.SecretReadError(e.getMessage)
      }
  }
}

class AwsVault(name: String, configuration: Configuration, _env: Env) extends Vault {

  private val logger = Logger("otoroshi-aws-vault")

  private val accessKey       =
    configuration.getOptionalWithFileSupport[String](s"access-key").getOrElse("key")
  //env.configuration.getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.access-key").getOrElse("key")
  private val accessKeySecret = configuration
    .getOptionalWithFileSupport[String](s"access-key-secret")
    .getOrElse("secret")
  //env.configuration
  //.getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.access-key-secret")
  //.getOrElse("secret")
  private val region          =
    configuration.getOptionalWithFileSupport[String](s"region").getOrElse("eu-west-3")
  //env.configuration.getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.region").getOrElse("eu-west-3")

  private val secretsManager = AWSSecretsManagerAsyncClientBuilder
    .standard()
    .withRegion(region)
    .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, accessKeySecret)))
    .build()

  override def get(path: String, options: Map[String, String])(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[CachedVaultSecretStatus] = {
    val promise = Promise.apply[CachedVaultSecretStatus]()
    try {
      val parts   = path.split("/").toSeq.filterNot(_.isEmpty)
      var request = new GetSecretValueRequest()
      request = request.withSecretId(parts.head)
      if (parts.size > 1) {
        request = request.withVersionId(parts.tail.head)
      }
      if (parts.size > 2) {
        request = request.withVersionStage(parts.tail.tail.head)
      }
      val handler = new AsyncHandler[GetSecretValueRequest, GetSecretValueResult]() {
        override def onError(exception: Exception): Unit =
          promise.trySuccess(CachedVaultSecretStatus.SecretReadError(exception.getMessage))
        override def onSuccess(request: GetSecretValueRequest, result: GetSecretValueResult): Unit = {
          promise.trySuccess(CachedVaultSecretStatus.SecretReadSuccess(result.getSecretString))
        }
      }
      secretsManager.getSecretValueAsync(request, handler)
    } catch {
      case e: Throwable => promise.trySuccess(CachedVaultSecretStatus.SecretReadError(e.getMessage))
    }
    promise.future
  }
}

class IzanamiVault(name: String, configuration: Configuration, _env: Env) extends Vault {

  private val logger = Logger("otoroshi-azure-vault")

  private val baseUrl      = configuration
    .getOptionalWithFileSupport[String](s"url")
    .getOrElse("https://127.0.0.1:9000")
  //env.configuration
  //.getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.url")
  //.getOrElse("https://127.0.0.1:9000")
  private val clientId     =
    configuration.getOptionalWithFileSupport[String](s"client-id").getOrElse("client")
  //env.configuration.getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.client-id").getOrElse("client")
  private val clientSecret =
    configuration.getOptionalWithFileSupport[String](s"client-secret").getOrElse("secret")
  //env.configuration.getOptionalWithFileSupport[String](s"otoroshi.vaults.${name}.client-secret").getOrElse("secret")

  private def dataUrl(id: String, options: Map[String, String]) = {
    val opts = if (options.nonEmpty) s"?" + options.toSeq.map(v => s"${v._1}=${v._2}").mkString("&") else ""
    s"${baseUrl}/api/configs/${id}${opts}"
  }

  override def get(path: String, options: Map[String, String])(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[CachedVaultSecretStatus] = {
    val parts     = path.split("/").toSeq.filterNot(_.isEmpty)
    val featureId = parts.head
    val pointer   = parts.tail.mkString("/", "/", "")
    val url       = dataUrl(featureId, options)
    env.Ws
      .url(url)
      .withAuth(clientId, clientSecret, WSAuthScheme.BASIC)
      .withRequestTimeout(1.minute)
      .withFollowRedirects(false)
      .get()
      .map { response =>
        if (response.status == 200) {
          response.json.atPointer(pointer).asOpt[JsValue] match {
            case Some(JsString(value))  => CachedVaultSecretStatus.SecretReadSuccess(value)
            case Some(JsNumber(value))  => CachedVaultSecretStatus.SecretReadSuccess(value.toString())
            case Some(JsBoolean(value)) => CachedVaultSecretStatus.SecretReadSuccess(value.toString)
            case Some(o @ JsObject(_))  => CachedVaultSecretStatus.SecretReadSuccess(o.stringify)
            case Some(arr @ JsArray(_)) => CachedVaultSecretStatus.SecretReadSuccess(arr.stringify)
            case Some(JsNull)           => CachedVaultSecretStatus.SecretReadSuccess("null")
            case _                      => CachedVaultSecretStatus.SecretValueNotFound
          }
        } else if (response.status == 401) {
          CachedVaultSecretStatus.SecretReadUnauthorized
        } else if (response.status == 403) {
          CachedVaultSecretStatus.SecretReadForbidden
        } else {
          CachedVaultSecretStatus.SecretReadError(response.status + " - " + response.body)
        }
      }
      .recover { case e: Throwable =>
        CachedVaultSecretStatus.SecretReadError(e.getMessage)
      }
  }
}

class SpringCloudConfigVault(name: String, configuration: Configuration, _env: Env) extends Vault {

  private val logger = Logger("otoroshi-spring-cloud-vault")

  private val baseUrl = configuration
    .getOptionalWithFileSupport[String](s"url")
    .getOrElse("http://127.0.0.1:8888")

  private val method  =
    configuration.getOptionalWithFileSupport[String](s"method").getOrElse("GET")
  private val headers =
    configuration.getOptionalWithFileSupport[Map[String, String]](s"headers").getOrElse(Map.empty).toSeq
  private val timeout =
    configuration
      .getOptionalWithFileSupport[Long](s"timeout")
      .map(v => FiniteDuration(v, TimeUnit.MILLISECONDS))
      .getOrElse(1.minute)
  private val root    =
    configuration.getOptionalWithFileSupport[String](s"root").getOrElse("foo/dev")

  override def get(path: String, options: Map[String, String])(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[CachedVaultSecretStatus] = {
    val parts   = path.split("/").toSeq.filterNot(_.isEmpty)
    val pointer = parts.mkString("/", "/", "")
    val url     = s"${baseUrl}/${root}"
    env.Ws
      .url(url)
      .withHttpHeaders(headers: _*)
      .withRequestTimeout(timeout)
      .withMethod(method)
      .withFollowRedirects(false)
      .execute()
      .map { response =>
        if (response.status == 200) {
          val sources = response.json
            .select("propertySources")
            .asOpt[Seq[JsValue]]
            .getOrElse(Seq.empty)
            .map(_.select("source").asOpt[JsObject].getOrElse(Json.obj()))
          val source  = sources.foldRight(Json.obj())((s, next) => s.deepMerge(next))
          source.atPointer(pointer).asOpt[JsValue] match {
            case Some(JsString(value))  => CachedVaultSecretStatus.SecretReadSuccess(value)
            case Some(JsNumber(value))  => CachedVaultSecretStatus.SecretReadSuccess(value.toString())
            case Some(JsBoolean(value)) => CachedVaultSecretStatus.SecretReadSuccess(value.toString)
            case Some(o @ JsObject(_))  => CachedVaultSecretStatus.SecretReadSuccess(o.stringify)
            case Some(arr @ JsArray(_)) => CachedVaultSecretStatus.SecretReadSuccess(arr.stringify)
            case Some(JsNull)           => CachedVaultSecretStatus.SecretReadSuccess("null")
            case _                      => CachedVaultSecretStatus.SecretValueNotFound
          }
        } else if (response.status == 401) {
          CachedVaultSecretStatus.SecretReadUnauthorized
        } else if (response.status == 403) {
          CachedVaultSecretStatus.SecretReadForbidden
        } else {
          CachedVaultSecretStatus.SecretReadError(response.status + " - " + response.body)
        }
      }
      .recover { case e: Throwable =>
        CachedVaultSecretStatus.SecretReadError(e.getMessage)
      }
  }
}

class HttpVault(name: String, configuration: Configuration, _env: Env) extends Vault {

  private val logger = Logger("otoroshi-http-vault")

  private val baseUrl = configuration
    .getOptionalWithFileSupport[String](s"url")
    .getOrElse("http://127.0.0.1:8888")

  private val method  =
    configuration.getOptionalWithFileSupport[String](s"method").getOrElse("GET")
  private val headers =
    configuration.getOptionalWithFileSupport[Map[String, String]](s"headers").getOrElse(Map.empty).toSeq
  private val timeout =
    configuration
      .getOptionalWithFileSupport[Long](s"timeout")
      .map(v => FiniteDuration(v, TimeUnit.MILLISECONDS))
      .getOrElse(1.minute)

  override def get(path: String, options: Map[String, String])(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[CachedVaultSecretStatus] = {
    val parts   = path.split("/").toSeq.filterNot(_.isEmpty)
    val pointer = parts.mkString("/", "/", "")
    val url     = s"${baseUrl}"
    env.Ws
      .url(url)
      .withHttpHeaders(headers: _*)
      .withRequestTimeout(timeout)
      .withMethod(method)
      .withFollowRedirects(false)
      .execute()
      .map { response =>
        if (response.status == 200) {
          val source = response.json
          source.atPointer(pointer).asOpt[JsValue] match {
            case Some(JsString(value))  => CachedVaultSecretStatus.SecretReadSuccess(value)
            case Some(JsNumber(value))  => CachedVaultSecretStatus.SecretReadSuccess(value.toString())
            case Some(JsBoolean(value)) => CachedVaultSecretStatus.SecretReadSuccess(value.toString)
            case Some(o @ JsObject(_))  => CachedVaultSecretStatus.SecretReadSuccess(o.stringify)
            case Some(arr @ JsArray(_)) => CachedVaultSecretStatus.SecretReadSuccess(arr.stringify)
            case Some(JsNull)           => CachedVaultSecretStatus.SecretReadSuccess("null")
            case _                      => CachedVaultSecretStatus.SecretValueNotFound
          }
        } else if (response.status == 401) {
          CachedVaultSecretStatus.SecretReadUnauthorized
        } else if (response.status == 403) {
          CachedVaultSecretStatus.SecretReadForbidden
        } else {
          CachedVaultSecretStatus.SecretReadError(response.status + " - " + response.body)
        }
      }
      .recover { case e: Throwable =>
        CachedVaultSecretStatus.SecretReadError(e.getMessage)
      }
  }
}

class InfisicalVault(name: String, configuration: Configuration, _env: Env) extends Vault {

  private val logger = Logger("otoroshi-infisical-vault")

  private val baseUrl = configuration
    .getOptionalWithFileSupport[String](s"baseUrl")
    .getOrElse("https://app.infisical.com")

  private val serviceToken = configuration
    .getOptionalWithFileSupport[String](s"serviceToken")
    .get

  private val e2ee = configuration
    .getOptionalWithFileSupport[Boolean](s"e2ee")
    .getOrElse(false)

  private val serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1)

  private val defaultSecretType: Option[String] = configuration
    .getOptionalWithFileSupport[String](s"defaultSecretType")

  private val defaultWorkspaceId: Option[String] = configuration
    .getOptionalWithFileSupport[String](s"defaultWorkspaceId")

  private val defaultEnvironment: Option[String] = configuration
    .getOptionalWithFileSupport[String](s"defaultEnvironment")

  private val timeout =
    configuration
      .getOptionalWithFileSupport[Long](s"timeout")
      .map(v => FiniteDuration(v, TimeUnit.MILLISECONDS))
      .getOrElse(1.minute)

  private val serviceTokenDataHolder = new AtomicReference[(JsValue, Option[String])](null)

  private def getServiceToken()(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[Either[String, (JsValue, Option[String])]] = {
    Option(serviceTokenDataHolder.get()) match {
      case Some(tuple) => tuple.right.vfuture
      case None        => {
        env.Ws
          .url(s"${baseUrl}/api/v2/service-token")
          .withRequestTimeout(timeout)
          .withHttpHeaders(
            "Accept"        -> "application/json",
            "Authorization" -> s"Bearer ${serviceToken}"
          )
          .get()
          .map { resp =>
            if (resp.status == 200) {
              val serviceTokenData = resp.json
              val secretKeyOpt     = decryptKey(serviceTokenData)
              val tuple            = (serviceTokenData, secretKeyOpt)
              serviceTokenDataHolder.set(tuple)
              Right(tuple)
            } else {
              Left(resp.body)
            }
          }
      }
    }
  }

  private def decryptKey(serviceTokenData: JsValue): Option[String] = {
    for {
      secretValueCiphertext <- serviceTokenData.select("encryptedKey").asOpt[String]
      secretValueIV         <- serviceTokenData.select("iv").asOpt[String]
      secretValueTag        <- serviceTokenData.select("tag").asOpt[String]
    } yield {
      val decryptedValue = {
        val ivBytes          = Base64.getDecoder().decode(secretValueIV)
        val tagBytes         = Base64.getDecoder().decode(secretValueTag)
        val keyBytes         = serviceTokenSecret.getBytes(Charsets.UTF_8)
        val encryptedBytes   = Base64.getDecoder().decode(secretValueCiphertext)
        val cipher           = Cipher.getInstance("AES/GCM/NoPadding")
        val keySpec          = new SecretKeySpec(keyBytes, "AES")
        val gcmParameterSpec = new GCMParameterSpec(128, ivBytes)
        cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec)
        new String(cipher.doFinal(Array.concat(encryptedBytes, tagBytes)), "UTF-8")
      }
      decryptedValue
    }
  }

  private def decryptValue(
      resp: JsValue,
      serviceTokenData: JsValue,
      serviceTokenKey: Option[String],
      options: Map[String, String]
  ): CachedVaultSecretStatus = {
    (for {
      secretKey             <- serviceTokenKey.orElse(decryptKey(serviceTokenData))
      secretValueCiphertext <- resp.select("secretValueCiphertext").asOpt[String]
      secretValueIV         <- resp.select("secretValueIV").asOpt[String]
      secretValueTag        <- resp.select("secretValueTag").asOpt[String]
    } yield {
      val decryptedValue = {
        val ivBytes          = Base64.getDecoder().decode(secretValueIV)
        val tagBytes         = Base64.getDecoder().decode(secretValueTag)
        val keyBytes         = secretKey.getBytes(Charsets.UTF_8)
        val encryptedBytes   = Base64.getDecoder().decode(secretValueCiphertext)
        val cipher           = Cipher.getInstance("AES/GCM/NoPadding")
        val keySpec          = new SecretKeySpec(keyBytes, "AES");
        val gcmParameterSpec = new GCMParameterSpec(128, ivBytes)
        cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec)
        new String(cipher.doFinal(Array.concat(encryptedBytes, tagBytes)))
      }
      selectValue(decryptedValue, options)
    }).getOrElse(CachedVaultSecretStatus.SecretReadError("error while reading encrypted secret value"))
  }

  private def selectValue(value: String, options: Map[String, String]): CachedVaultSecretStatus = try {
    options.get("json_pointer") match {
      case None          => CachedVaultSecretStatus.SecretReadSuccess(value)
      case Some(pointer) =>
        Json.parse(value).atPointer(pointer).asOpt[JsValue] match {
          case Some(JsString(value))  => CachedVaultSecretStatus.SecretReadSuccess(value)
          case Some(JsNumber(value))  => CachedVaultSecretStatus.SecretReadSuccess(value.toString())
          case Some(JsBoolean(value)) => CachedVaultSecretStatus.SecretReadSuccess(value.toString)
          case Some(o @ JsObject(_))  => CachedVaultSecretStatus.SecretReadSuccess(o.stringify)
          case Some(arr @ JsArray(_)) => CachedVaultSecretStatus.SecretReadSuccess(arr.stringify)
          case Some(JsNull)           => CachedVaultSecretStatus.SecretReadSuccess("null")
          case _                      => CachedVaultSecretStatus.SecretValueNotFound
        }
    }
  } catch {
    case e: Throwable =>
      logger.error(s"error while selecting json", e)
      CachedVaultSecretStatus.SecretReadError(e.getMessage)
  }

  override def get(_path: String, options: Map[String, String])(implicit
      env: Env,
      ec: ExecutionContext
  ): Future[CachedVaultSecretStatus] = {
    getServiceToken()
      .flatMap {
        case Left(err)                               => CachedVaultSecretStatus.SecretReadError(err).vfuture
        case Right((serviceTokenData, secretKeyOpt)) => {
          val path                        = _path.tail
          val url                         = if (e2ee) {
            s"$baseUrl/api/v3/secrets/${path}"
          } else {
            s"$baseUrl/api/v3/secrets/raw/${path}"
          }
          val workspaceId: Option[String] = defaultWorkspaceId
            .orElse(options.get("workspaceId"))
            .orElse(serviceTokenData.select("workspace").asOpt[String])
          val environment: Option[String] = defaultEnvironment
            .orElse(options.get("environment"))
            .orElse(serviceTokenData.select("scopes").select(0).select("environment").asOpt[String])
          val secretType: Option[String]  = defaultSecretType
            .orElse(options.get("type"))
          val finalUrl                    = url
            .applyOnWithOpt(workspaceId) { case (url, w) =>
              if (url.contains("?")) s"$url&workspaceId=${w}" else s"$url?workspaceId=${w}"
            }
            .applyOnWithOpt(environment) { case (url, e) =>
              if (url.contains("?")) s"$url&environment=${e}" else s"$url?environment=${e}"
            }
            .applyOnWithOpt(secretType) { case (url, s) =>
              if (url.contains("?")) s"$url&type=${s}" else s"$url?type=${s}"
            }
          env.Ws
            .url(finalUrl)
            .withRequestTimeout(timeout)
            .withHttpHeaders(
              "Accept"        -> "application/json",
              "Authorization" -> s"Bearer ${serviceToken}"
            )
            .get()
            .map { resp =>
              if (resp.status == 200) {
                if (e2ee) {
                  decryptValue(
                    resp.json.select("secret").asOpt[JsObject].getOrElse(Json.obj()),
                    serviceTokenData,
                    secretKeyOpt,
                    options
                  )
                } else {
                  resp.json.select("secret").select("secretValue").asOpt[String] match {
                    case None              =>
                      CachedVaultSecretStatus.SecretReadError(s"unable to read secret ${path} in ${resp.json}")
                    case Some(secretValue) => selectValue(secretValue, options)
                  }
                }
              } else if (resp.status == 401) {
                CachedVaultSecretStatus.SecretReadUnauthorized
              } else if (resp.status == 403) {
                CachedVaultSecretStatus.SecretReadForbidden
              } else if (resp.status == 404) {
                CachedVaultSecretStatus.SecretNotFound
              } else {
                CachedVaultSecretStatus.SecretReadError(resp.status + " - " + resp.body)
              }
            }
        }
      }
      .recover { case e: Throwable =>
        e.printStackTrace()
        CachedVaultSecretStatus.SecretReadError(e.getMessage)
      }
  }
}

class Vaults(env: Env) {

  private val logger              = Logger("otoroshi-vaults")
  private val vaultConfig         =
    env._configuration.getOptionalWithFileSupport[Configuration]("otoroshi.vaults").getOrElse(Configuration.empty)
  private val secretsTtl          = vaultConfig
    .getOptionalWithFileSupport[Long]("secrets-ttl")
    .map(_.milliseconds)
    .getOrElse(5.minutes)
  private val secretsErrorTtl     = vaultConfig
    .getOptionalWithFileSupport[Long]("secrets-error-ttl")
    .map(_.milliseconds)
    .getOrElse(20.seconds)
  private val readTtl             = vaultConfig
    .getOptionalWithFileSupport[Long]("read-timeout")
    .map(_.milliseconds)
    .getOrElse(10.seconds)
  private val parallelFetchs      = vaultConfig
    .getOptionalWithFileSupport[Int]("parallel-fetchs")
    .getOrElse(4)
  private val cachedSecrets: Long =
    vaultConfig.getOptionalWithFileSupport[Long]("cached-secrets").getOrElse(10000L)

  val leaderFetchOnly: Boolean =
    vaultConfig.getOptionalWithFileSupport[Boolean]("leader-fetch-only").getOrElse(false)

  private val cache                          = Caches.bounded[String, CachedVaultSecret](cachedSecrets.toInt)
  // Scaffeine().expireAfterWrite(secretsTtl).maximumSize(cachedSecrets).build[String, CachedVaultSecret]()
  private val expressionReplacer             = ReplaceAllWith("\\$\\{vault://([^}]*)\\}")
  private val vaults: TrieMap[String, Vault] = new UnboundedTrieMap[String, Vault]()
  private implicit val _env                  = env
  private implicit val ec                    = env.otoroshiExecutionContext

  val enabled: Boolean =
    vaultConfig.getOptionalWithFileSupport[Boolean]("enabled").getOrElse(false)

  if (enabled) {
    logger.warn("the vaults feature is enabled !")
    logger.warn("be aware that this feature is EXPERIMENTAL and might not work as expected.")
    val vaultsConfig = vaultConfig.json
    vaultsConfig.keys.map { key =>
      vaultsConfig.select(key).asOpt[JsObject].map { vault =>
        val typ = vault.select("type").asOpt[String].getOrElse("env")
        if (typ == "env") {
          logger.info(s"a vault named '${key}' of kind '${typ}' is now active !")
          vaults.put(key, new EnvVault(key, vaultConfig.get[Configuration](key), env))
        } else if (typ == "local") {
          logger.info(s"a vault named '${key}' of kind '${typ}' is now active !")
          vaults.put(key, new LocalVault(key, vaultConfig.get[Configuration](key), env))
        } else if (typ == "hashicorp-vault") {
          logger.info(s"a vault named '${key}' of kind '${typ}' is now active !")
          vaults.put(key, new HashicorpVault(key, vaultConfig.get[Configuration](key), env))
        } else if (typ == "azure") {
          logger.info(s"a vault named '${key}' of kind '${typ}' is now active !")
          vaults.put(key, new AzureVault(key, vaultConfig.get[Configuration](key), env))
        } else if (typ == "aws") {
          logger.info(s"a vault named '${key}' of kind '${typ}' is now active !")
          vaults.put(key, new AwsVault(key, vaultConfig.get[Configuration](key), env))
        } else if (typ == "kubernetes") {
          logger.info(s"a vault named '${key}' of kind '${typ}' is now active !")
          vaults.put(key, new KubernetesVault(key, vaultConfig.get[Configuration](key), env))
        } else if (typ == "izanami") {
          logger.info(s"a vault named '${key}' of kind '${typ}' is now active !")
          vaults.put(key, new IzanamiVault(key, vaultConfig.get[Configuration](key), env))
        } else if (typ == "spring-cloud") {
          logger.info(s"a vault named '${key}' of kind '${typ}' is now active !")
          vaults.put(key, new SpringCloudConfigVault(key, vaultConfig.get[Configuration](key), env))
        } else if (typ == "http") {
          logger.info(s"a vault named '${key}' of kind '${typ}' is now active !")
          vaults.put(key, new HttpVault(key, vaultConfig.get[Configuration](key), env))
        } else if (typ == "gcloud") {
          logger.info(s"a vault named '${key}' of kind '${typ}' is now active !")
          vaults.put(key, new GoogleSecretManagerVault(key, vaultConfig.get[Configuration](key), env))
        } else if (typ == "alibaba-cloud") {
          logger.info(s"a vault named '${key}' of kind '${typ}' is now active !")
          vaults.put(key, new AlibabaCloudSecretManagerVault(key, vaultConfig.get[Configuration](key), env))
        } else if (typ == "infisical") {
          logger.info(s"a vault named '${key}' of kind '${typ}' is now active !")
          vaults.put(key, new InfisicalVault(key, vaultConfig.get[Configuration](key), env))
        } else {
          // TODO: support square https://github.com/square/keywhiz ?
          // TODO: support pinterest https://github.com/pinterest/knox ?
          env.adminExtensions.vault(typ) match {
            case None             => logger.error(s"unknown vault type '${typ}'")
            case Some(adminVault) => {
              logger.info(s"a vault named '${key}' of kind '${typ}' is now active !")
              vaults.put(key, adminVault.build(key, vaultConfig.get[Configuration](key), env))
            }
          }
        }
      }
    }
  }

  def timeout(status: CachedVaultSecretStatus): Future[CachedVaultSecretStatus] = {
    val promise = Promise[CachedVaultSecretStatus]()
    env.otoroshiScheduler.scheduleOnce(readTtl) {
      promise.trySuccess(status)
    }
    promise.future
  }

  def getWithTimeout(vault: Vault, path: String, options: Map[String, String]): Future[CachedVaultSecretStatus] = {
    Future.firstCompletedOf(
      Seq(
        vault.get(path, options),
        timeout(CachedVaultSecretStatus.SecretReadTimeout)
      )
    )
  }

  def resolveExpression(expr: String, force: Boolean): Future[CachedVaultSecretStatus] = {
    val uri     = Uri(expr)
    val name    = uri.authority.host.toString()
    val path    = uri.path.toString()
    val options = uri.query().toMap

    def fetchSecret(): Future[CachedVaultSecretStatus] = {
      vaults.get(name) match {
        case None        =>
          cache.put(expr, CachedVaultSecret(expr, DateTime.now(), CachedVaultSecretStatus.VaultNotFound))
          CachedVaultSecretStatus.VaultNotFound.vfuture
        case Some(vault) => {
          getWithTimeout(vault, path, options)
            .map { status =>
              val theStatus = status match {
                case CachedVaultSecretStatus.SecretReadSuccess(v) =>
                  val computed = JsString(v).stringify.substring(1).init
                  CachedVaultSecretStatus.SecretReadSuccess(computed)
                case s                                            => s
              }
              val secret    = CachedVaultSecret(expr, DateTime.now(), theStatus)
              cache.put(expr, secret)
              theStatus
            }
            .recover {
              case e: Throwable => {
                val secret =
                  CachedVaultSecret(expr, DateTime.now(), CachedVaultSecretStatus.SecretReadError(e.getMessage))
                cache.put(expr, secret)
                secret.status
              }
            }
        }
      }
    }

    if (force) {
      fetchSecret()
    } else {
      cache.getIfPresent(expr) match {
        case Some(res) => res.status.vfuture
        case None      => fetchSecret()
      }
    }
  }

  def renewSecretsInCache(): Future[Done] = {
    if (enabled) {
      Source(cache.asMap().values.toList)
        .filter { secret =>
          secret.status match {
            case CachedVaultSecretStatus.SecretReadSuccess(_)
                if (System.currentTimeMillis() - secret.at.toDate.getTime) < (secretsTtl.toMillis - 20000) =>
              false
            case _ => true
          }
        }
        .mapAsync(parallelFetchs) { secret =>
          def resolve(force: Boolean): Future[Unit] = {
            resolveExpression(secret.key, force)
              .map(_ => ())
              .recover { case e: Throwable =>
                ()
              }
          }

          val shouldRetry: Boolean = (System.currentTimeMillis() - secret.at.toDate.getTime) > secretsErrorTtl.toMillis
          secret.status match {
            case CachedVaultSecretStatus.SecretReadSuccess(_) => resolve(force = false)
            case CachedVaultSecretStatus.VaultNotFound        => resolve(force = false)
            case _ if shouldRetry                             => resolve(force = true)
            case _ if !shouldRetry                            => resolve(force = false)
          }
        }
        .runWith(Sink.ignore)(env.otoroshiMaterializer)
    } else {
      Done.vfuture
    }
  }

  // private def fillSecrets(source: String): String = {
  //   if (enabled) {
  //     expressionReplacer.replaceOn(source) { expr =>
  //       val status = Await.result(resolveExpression(expr), 1.minute)
  //       status match {
  //         case CachedVaultSecretStatus.SecretReadSuccess(_) => logger.debug(s"fill secret from '${expr}' successfully")
  //         case _ => logger.error(s"filling secret from '${expr}' failed because of '${status.value}'")
  //       }
  //       status.value
  //     }
  //   } else {
  //     source
  //   }
  // }

  def fillSecretsAsync(id: String, source: String)(implicit ec: ExecutionContext): Future[String] = {
    if (enabled) {
      expressionReplacer.replaceOnAsync(source) { expr =>
        resolveExpression(expr, force = false)
          .map {
            case CachedVaultSecretStatus.SecretReadSuccess(value) =>
              if (logger.isDebugEnabled)
                logger.debug(s"fill secret on '${id}' from '${expr}' successfully") //, secret value is '${value}'")
              value
            case status                                           =>
              logger.error(s"filling secret on '${id}' from '${expr}' failed because of '${status.value}'")
              "not-found"
          }
          .recover { case e: Throwable =>
            val error = CachedVaultSecretStatus.SecretReadError(e.getMessage)
            logger.error(s"filling secret on '${id}' from '${expr}' failed because of '${error.value}'")
            "not-found"
          }
      }
    } else {
      source.vfuture
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy