diff --git a/build.sbt b/build.sbt index 4e57fe4..75a7287 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,9 @@ import Settings.modulesSettings import Settings.secretProviderDeps +import Settings.artifactVersion import Settings.AssemblyConfigurator +import Settings.testSinkDeps +import Settings.scala213 import sbt.Project.projectToLocalProject name := "secret-provider" @@ -8,12 +11,15 @@ javacOptions ++= Seq("--release", "11") javaOptions ++= Seq("-Xms512M", "-Xmx2048M") lazy val subProjects: Seq[Project] = Seq( + `test-sink`, `secret-provider`, ) -lazy val subProjectsRefs: Seq[ProjectReference] = subProjects.map(projectToLocalProject) +lazy val subProjectsRefs: Seq[ProjectReference] = + subProjects.map(projectToLocalProject) lazy val root = (project in file(".")) .settings( + scalaVersion := scala213, publish := {}, publishArtifact := false, name := "secret-provider", @@ -34,12 +40,26 @@ lazy val `secret-provider` = (project in file("secret-provider")) ), ) .configureAssembly() + .configs(IntegrationTest) + .settings(Defaults.itSettings: _*) + +lazy val `test-sink` = (project in file("test-sink")) + .settings( + modulesSettings ++ + Seq( + name := "test-sink", + description := "Kafka Connect compatible connectors to move data between Kafka and popular data stores", + publish / skip := true, + libraryDependencies ++= testSinkDeps, + ), + ) + .configureAssembly() addCommandAlias( "validateAll", - ";scalafmtCheck;scalafmtSbtCheck;test:scalafmtCheck;", + ";scalafmtCheck;scalafmtSbtCheck;test:scalafmtCheck;it:scalafmtCheck;", ) addCommandAlias( "formatAll", - ";scalafmt;scalafmtSbt;test:scalafmt;", + ";scalafmt;scalafmtSbt;test:scalafmt;it:scalafmt;", ) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 60af3a8..56a744f 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -13,20 +13,22 @@ trait Dependencies { val vaultVersion = "5.1.0" val azureKeyVaultVersion = "4.5.2" val azureIdentityVersion = "1.8.0" - val awsSecretsVersion = "1.12.411" + val awsSecretsVersion = "1.12.420" //test - val scalaTestVersion = "3.2.15" - val mockitoVersion = "3.2.15.0" - val byteBuddyVersion = "1.14.0" - val slf4jVersion = "2.0.5" - val commonsIOVersion = "1.3.2" - val jettyVersion = "11.0.13" - val testContainersVersion = "1.12.3" - val flexmarkVersion = "0.64.0" + val scalaTestVersion = "3.2.15" + val mockitoVersion = "3.2.15.0" + val byteBuddyVersion = "1.14.1" + val slf4jVersion = "2.0.5" + val commonsIOVersion = "1.3.2" + val jettyVersion = "11.0.13" + val flexmarkVersion = "0.64.0" val scalaCollectionCompatVersion = "2.8.1" val jakartaServletVersion = "6.0.0" + val testContainersVersion = "1.17.6" + val json4sVersion = "4.0.6" + val catsEffectVersion = "3.4.8" } @@ -59,6 +61,15 @@ trait Dependencies { val `jakartaServlet` = "jakarta.servlet" % "jakarta.servlet-api" % jakartaServletVersion + val `testContainersCore` = + "org.testcontainers" % "testcontainers" % testContainersVersion + val `testContainersKafka` = + "org.testcontainers" % "kafka" % testContainersVersion + val `testContainersVault` = + "org.testcontainers" % "vault" % testContainersVersion + val `json4sNative` = "org.json4s" %% "json4s-native" % json4sVersion + val `json4sJackson` = "org.json4s" %% "json4s-jackson" % json4sVersion + val `cats` = "org.typelevel" %% "cats-effect" % catsEffectVersion } import Dependencies._ @@ -69,16 +80,27 @@ trait Dependencies { `azure-key-vault`, `azure-identity` exclude ("javax.activation", "activation"), `aws-secrets-manager`, - `scalaCollectionCompat`, - `jakartaServlet` % Test, - `mockito` % Test, - `byteBuddy` % Test, - `scalatest` % Test, - `jetty` % Test, - `commons-io` % Test, - `flexmark` % Test, - `slf4j-api` % Test, - `slf4j-simple` % Test, + `jakartaServlet` % Test, + `mockito` % Test, + `byteBuddy` % Test, + `scalatest` % Test, + `jetty` % Test, + `commons-io` % Test, + `flexmark` % Test, + `slf4j-api` % Test, + `slf4j-simple` % Test, + `cats` % IntegrationTest, + `testContainersCore` % IntegrationTest, + `testContainersKafka` % IntegrationTest, + `testContainersVault` % IntegrationTest, + `json4sNative` % IntegrationTest, + ) + + val testSinkDeps = Seq( + `scala-logging`, + `kafka-connect-api` % Provided, + `vault-java-driver`, + `scalatest` % Test, ) } diff --git a/project/Settings.scala b/project/Settings.scala index d7a8ff5..e760da5 100644 --- a/project/Settings.scala +++ b/project/Settings.scala @@ -2,15 +2,16 @@ * Copyright (c) 2017-2020 Lenses.io Ltd */ +import com.eed3si9n.jarjarabrams.ShadeRule import sbt.Keys._ import sbt.Def import sbt._ import sbtassembly.AssemblyKeys._ import sbtassembly.MergeStrategy +import sbtassembly.PathList object Settings extends Dependencies { - val scala212 = "2.12.14" val scala213 = "2.13.10" val scala3 = "3.2.2" @@ -37,7 +38,7 @@ object Settings extends Dependencies { Resolver.mavenLocal, Resolver.mavenCentral, ), - crossScalaVersions := List( /*scala3, */ scala213 /*scala212*/ ), + crossScalaVersions := List( /*scala3, */ scala213), Compile / scalacOptions ++= Seq( "-release:11", "-encoding", @@ -58,33 +59,63 @@ object Settings extends Dependencies { ScalacFlags.WarnUnusedImports212, ScalacFlags.WarnUnusedImports213, ), + // TODO remove for cross build + scalaVersion := scala213, Test / fork := true, ) implicit final class AssemblyConfigurator(project: Project) { + + /* + Exclude the jar signing files from dependencies. They come from other jars, + and including these would break assembly (due to duplicates) or classloading + (due to mismatching jar checksum/signature). + */ + val excludeFilePatterns = Set(".MF", ".RSA", ".DSA", ".SF") + + def excludeFileFilter(p: String): Boolean = + excludeFilePatterns.exists(p.endsWith) + + val excludePatterns = Set( + "kafka-client", + "kafka-connect-json", + "hadoop-yarn", + "org.apache.kafka", + "zookeeper", + "log4j", + "junit", + ) + def configureAssembly(): Project = project.settings( - assembly / assemblyJarName := s"secret-provider_${SemanticVersioning(scalaVersion.value).majorMinor}-${artifactVersion}-all.jar", - assembly / assemblyExcludedJars := { - val cp = (assembly / fullClasspath).value - val excludes = Set( - "org.apache.avro", - "org.apache.kafka", - "io.confluent", - "org.apache.zookeeper", - ) - cp filter { f => excludes.exists(f.data.getName.contains(_)) } - }, - assembly / assemblyMergeStrategy := { - case "module-info.class" => MergeStrategy.discard - case x if x.contains("io.netty.versions.properties") => - MergeStrategy.concat - case x => - val oldStrategy = (assembly / assemblyMergeStrategy).value - oldStrategy(x) - }, + Seq( + assembly / assemblyOutputPath := file( + target.value + "/libs/" + (assembly / assemblyJarName).value, + ), + assembly / assemblyExcludedJars := { + val cp: Classpath = (assembly / fullClasspath).value + cp filter { f => + excludePatterns + .exists(f.data.getName.contains) && (!f.data.getName + .contains("slf4j")) + } + }, + assembly / assemblyMergeStrategy := { + case PathList("META-INF", "MANIFEST.MF") => MergeStrategy.discard + case p if excludeFileFilter(p) => MergeStrategy.discard + case PathList(ps @ _*) if ps.last == "module-info.class" => + MergeStrategy.discard + case _ => MergeStrategy.first + }, + assembly / assemblyShadeRules ++= Seq( + ShadeRule.rename("io.confluent.**" -> "lshaded.confluent.@1").inAll, + ShadeRule.rename("com.fasterxml.**" -> "lshaded.fasterxml.@1").inAll, + ), + ), ) } + lazy val IntegrationTest = config("it") extend Test + } diff --git a/secret-provider/src/it/resources/logback.xml b/secret-provider/src/it/resources/logback.xml new file mode 100644 index 0000000..ad0267a --- /dev/null +++ b/secret-provider/src/it/resources/logback.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + System.out + + %d{ISO8601} %-5p %X{dbz.connectorType}|%X{dbz.connectorName}|%X{dbz.connectorContext} %m [%c]%n + + + + + + + + + \ No newline at end of file diff --git a/secret-provider/src/it/resources/openapi.json b/secret-provider/src/it/resources/openapi.json new file mode 100644 index 0000000..2b38461 --- /dev/null +++ b/secret-provider/src/it/resources/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.0.2","info":{"title":"HashiCorp Vault API","description":"HTTP API that gives you full access to Vault. All API routes are prefixed with `/v1/`.","version":"1.12.3","license":{"name":"Mozilla Public License 2.0","url":"https://www.mozilla.org/en-US/MPL/2.0"}},"paths":{"/auth/token/accessors/":{"description":"List token accessors, which can then be be used to iterate and discover their properties or revoke them. Because this can be used to cause a denial of service, this endpoint requires 'sudo' capability in addition to 'list'.","x-vault-sudo":true,"get":{"summary":"List token accessors, which can then be\nbe used to iterate and discover their properties\nor revoke them. Because this can be used to\ncause a denial of service, this endpoint\nrequires 'sudo' capability in addition to\n'list'.","operationId":"getAuthTokenAccessors","tags":["auth"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/auth/token/create":{"description":"The token create path is used to create new tokens.","parameters":[{"name":"format","description":"Return json formatted output","in":"query","schema":{"type":"string"}}],"post":{"summary":"The token create path is used to create new tokens.","operationId":"postAuthTokenCreate","tags":["auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenCreateRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/auth/token/create-orphan":{"description":"The token create path is used to create new orphan tokens.","parameters":[{"name":"format","description":"Return json formatted output","in":"query","schema":{"type":"string"}}],"post":{"summary":"The token create path is used to create new orphan tokens.","operationId":"postAuthTokenCreateOrphan","tags":["auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenCreateOrphanRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/auth/token/create/{role_name}":{"description":"This token create path is used to create new tokens adhering to the given role.","parameters":[{"name":"format","description":"Return json formatted output","in":"query","schema":{"type":"string"}},{"name":"role_name","description":"Name of the role","in":"path","schema":{"type":"string"},"required":true}],"post":{"summary":"This token create path is used to create new tokens adhering to the given role.","operationId":"postAuthTokenCreateRole_name","tags":["auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenCreateRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/auth/token/lookup":{"description":"This endpoint will lookup a token and its properties.","get":{"summary":"This endpoint will lookup a token and its properties.","operationId":"getAuthTokenLookup","tags":["auth"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"This endpoint will lookup a token and its properties.","operationId":"postAuthTokenLookup","tags":["auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenLookupRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/auth/token/lookup-accessor":{"description":"This endpoint will lookup a token associated with the given accessor and its properties. Response will not contain the token ID.","post":{"summary":"This endpoint will lookup a token associated with the given accessor and its properties. Response will not contain the token ID.","operationId":"postAuthTokenLookupAccessor","tags":["auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenLookupAccessorRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/auth/token/lookup-self":{"description":"This endpoint will lookup a token and its properties.","get":{"summary":"This endpoint will lookup a token and its properties.","operationId":"getAuthTokenLookupSelf","tags":["auth"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"This endpoint will lookup a token and its properties.","operationId":"postAuthTokenLookupSelf","tags":["auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenLookupSelfRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/auth/token/renew":{"description":"This endpoint will renew the given token and prevent expiration.","post":{"summary":"This endpoint will renew the given token and prevent expiration.","operationId":"postAuthTokenRenew","tags":["auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRenewRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/auth/token/renew-accessor":{"description":"This endpoint will renew a token associated with the given accessor and its properties. Response will not contain the token ID.","post":{"summary":"This endpoint will renew a token associated with the given accessor and its properties. Response will not contain the token ID.","operationId":"postAuthTokenRenewAccessor","tags":["auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRenewAccessorRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/auth/token/renew-self":{"description":"This endpoint will renew the token used to call it and prevent expiration.","post":{"summary":"This endpoint will renew the token used to call it and prevent expiration.","operationId":"postAuthTokenRenewSelf","tags":["auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRenewSelfRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/auth/token/revoke":{"description":"This endpoint will delete the given token and all of its child tokens.","post":{"summary":"This endpoint will delete the given token and all of its child tokens.","operationId":"postAuthTokenRevoke","tags":["auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRevokeRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/auth/token/revoke-accessor":{"description":"This endpoint will delete the token associated with the accessor and all of its child tokens.","post":{"summary":"This endpoint will delete the token associated with the accessor and all of its child tokens.","operationId":"postAuthTokenRevokeAccessor","tags":["auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRevokeAccessorRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/auth/token/revoke-orphan":{"description":"This endpoint will delete the token and orphan its child tokens.","post":{"summary":"This endpoint will delete the token and orphan its child tokens.","operationId":"postAuthTokenRevokeOrphan","tags":["auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRevokeOrphanRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/auth/token/revoke-self":{"description":"This endpoint will delete the token used to call it and all of its child tokens.","post":{"summary":"This endpoint will delete the token used to call it and all of its child tokens.","operationId":"postAuthTokenRevokeSelf","tags":["auth"],"responses":{"200":{"description":"OK"}}}},"/auth/token/roles":{"description":"This endpoint lists configured roles.","get":{"summary":"This endpoint lists configured roles.","operationId":"getAuthTokenRoles","tags":["auth"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/auth/token/roles/{role_name}":{"parameters":[{"name":"role_name","description":"Name of the role","in":"path","schema":{"type":"string"},"required":true}],"x-vault-createSupported":true,"get":{"operationId":"getAuthTokenRolesRole_name","tags":["auth"],"responses":{"200":{"description":"OK"}}},"post":{"operationId":"postAuthTokenRolesRole_name","tags":["auth"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRolesRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"operationId":"deleteAuthTokenRolesRole_name","tags":["auth"],"responses":{"204":{"description":"empty body"}}}},"/auth/token/tidy":{"description":"This endpoint performs cleanup tasks that can be run if certain error conditions have occurred.","post":{"summary":"This endpoint performs cleanup tasks that can be run if certain error\nconditions have occurred.","operationId":"postAuthTokenTidy","tags":["auth"],"responses":{"200":{"description":"OK"}}}},"/cubbyhole/{path}":{"description":"Pass-through secret storage to a token-specific cubbyhole in the storage backend, allowing you to read/write arbitrary data into secret storage.","parameters":[{"name":"path","description":"Specifies the path of the secret.","in":"path","schema":{"type":"string"},"required":true}],"x-vault-createSupported":true,"get":{"summary":"Retrieve the secret at the specified location.","operationId":"getCubbyholePath","tags":["secrets"],"parameters":[{"name":"list","description":"Return a list if `true`","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Store a secret at the specified location.","operationId":"postCubbyholePath","tags":["secrets"],"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Deletes the secret at the specified location.","operationId":"deleteCubbyholePath","tags":["secrets"],"responses":{"204":{"description":"empty body"}}}},"/identity/alias":{"description":"Create a new alias.","post":{"summary":"Create a new alias.","operationId":"postIdentityAlias","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityAliasRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/alias/id":{"description":"List all the alias IDs.","get":{"summary":"List all the alias IDs.","operationId":"getIdentityAliasId","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/alias/id/{id}":{"description":"Update, read or delete an alias ID.","parameters":[{"name":"id","description":"ID of the alias","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Update, read or delete an alias ID.","operationId":"getIdentityAliasIdId","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Update, read or delete an alias ID.","operationId":"postIdentityAliasIdId","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityAliasIdRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Update, read or delete an alias ID.","operationId":"deleteIdentityAliasIdId","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/entity":{"description":"Create a new entity","post":{"summary":"Create a new entity","operationId":"postIdentityEntity","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityEntityRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/entity-alias":{"description":"Create a new alias.","post":{"summary":"Create a new alias.","operationId":"postIdentityEntityAlias","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityEntityAliasRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/entity-alias/id":{"description":"List all the alias IDs.","get":{"summary":"List all the alias IDs.","operationId":"getIdentityEntityAliasId","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/entity-alias/id/{id}":{"description":"Update, read or delete an alias ID.","parameters":[{"name":"id","description":"ID of the alias","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Update, read or delete an alias ID.","operationId":"getIdentityEntityAliasIdId","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Update, read or delete an alias ID.","operationId":"postIdentityEntityAliasIdId","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityEntityAliasIdRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Update, read or delete an alias ID.","operationId":"deleteIdentityEntityAliasIdId","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/entity/batch-delete":{"description":"Delete all of the entities provided","post":{"summary":"Delete all of the entities provided","operationId":"postIdentityEntityBatchDelete","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityEntityBatchDeleteRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/entity/id":{"description":"List all the entity IDs","get":{"summary":"List all the entity IDs","operationId":"getIdentityEntityId","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/entity/id/{id}":{"description":"Update, read or delete an entity using entity ID","parameters":[{"name":"id","description":"ID of the entity. If set, updates the corresponding existing entity.","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Update, read or delete an entity using entity ID","operationId":"getIdentityEntityIdId","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Update, read or delete an entity using entity ID","operationId":"postIdentityEntityIdId","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityEntityIdRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Update, read or delete an entity using entity ID","operationId":"deleteIdentityEntityIdId","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/entity/merge":{"description":"Merge two or more entities together","post":{"summary":"Merge two or more entities together","operationId":"postIdentityEntityMerge","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityEntityMergeRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/entity/name":{"description":"List all the entity names","get":{"summary":"List all the entity names","operationId":"getIdentityEntityName","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/entity/name/{name}":{"description":"Update, read or delete an entity using entity name","parameters":[{"name":"name","description":"Name of the entity","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Update, read or delete an entity using entity name","operationId":"getIdentityEntityNameName","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Update, read or delete an entity using entity name","operationId":"postIdentityEntityNameName","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityEntityNameRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Update, read or delete an entity using entity name","operationId":"deleteIdentityEntityNameName","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/group":{"description":"Create a new group.","post":{"summary":"Create a new group.","operationId":"postIdentityGroup","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityGroupRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/group-alias":{"description":"Creates a new group alias, or updates an existing one.","post":{"summary":"Creates a new group alias, or updates an existing one.","operationId":"postIdentityGroupAlias","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityGroupAliasRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/group-alias/id":{"description":"List all the group alias IDs.","get":{"summary":"List all the group alias IDs.","operationId":"getIdentityGroupAliasId","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/group-alias/id/{id}":{"parameters":[{"name":"id","description":"ID of the group alias.","in":"path","schema":{"type":"string"},"required":true}],"get":{"operationId":"getIdentityGroupAliasIdId","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"operationId":"postIdentityGroupAliasIdId","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityGroupAliasIdRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"operationId":"deleteIdentityGroupAliasIdId","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/group/id":{"description":"List all the group IDs.","get":{"summary":"List all the group IDs.","operationId":"getIdentityGroupId","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/group/id/{id}":{"description":"Update or delete an existing group using its ID.","parameters":[{"name":"id","description":"ID of the group. If set, updates the corresponding existing group.","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Update or delete an existing group using its ID.","operationId":"getIdentityGroupIdId","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Update or delete an existing group using its ID.","operationId":"postIdentityGroupIdId","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityGroupIdRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Update or delete an existing group using its ID.","operationId":"deleteIdentityGroupIdId","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/group/name":{"get":{"operationId":"getIdentityGroupName","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/group/name/{name}":{"parameters":[{"name":"name","description":"Name of the group.","in":"path","schema":{"type":"string"},"required":true}],"get":{"operationId":"getIdentityGroupNameName","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"operationId":"postIdentityGroupNameName","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityGroupNameRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"operationId":"deleteIdentityGroupNameName","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/lookup/entity":{"description":"Query entities based on various properties.","post":{"summary":"Query entities based on various properties.","operationId":"postIdentityLookupEntity","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityLookupEntityRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/lookup/group":{"description":"Query groups based on various properties.","post":{"summary":"Query groups based on various properties.","operationId":"postIdentityLookupGroup","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityLookupGroupRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/mfa/login-enforcement":{"get":{"summary":"List login enforcements","operationId":"getIdentityMfaLoginEnforcement","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/mfa/login-enforcement/{name}":{"parameters":[{"name":"name","description":"Name for this login enforcement configuration","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Read the current login enforcement","operationId":"getIdentityMfaLoginEnforcementName","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Create or update a login enforcement","operationId":"postIdentityMfaLoginEnforcementName","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityMfaLoginEnforcementRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Delete a login enforcement","operationId":"deleteIdentityMfaLoginEnforcementName","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/mfa/method":{"get":{"summary":"List MFA method configurations for all MFA methods","operationId":"getIdentityMfaMethod","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/mfa/method/duo":{"get":{"summary":"List MFA method configurations for the given MFA method","operationId":"getIdentityMfaMethodDuo","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/mfa/method/duo/{method_id}":{"parameters":[{"name":"method_id","description":"The unique identifier for this MFA method.","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Read the current configuration for the given MFA method","operationId":"getIdentityMfaMethodDuoMethod_id","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Update or create a configuration for the given MFA method","operationId":"postIdentityMfaMethodDuoMethod_id","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityMfaMethodDuoRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Delete a configuration for the given MFA method","operationId":"deleteIdentityMfaMethodDuoMethod_id","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/mfa/method/okta":{"get":{"summary":"List MFA method configurations for the given MFA method","operationId":"getIdentityMfaMethodOkta","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/mfa/method/okta/{method_id}":{"parameters":[{"name":"method_id","description":"The unique identifier for this MFA method.","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Read the current configuration for the given MFA method","operationId":"getIdentityMfaMethodOktaMethod_id","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Update or create a configuration for the given MFA method","operationId":"postIdentityMfaMethodOktaMethod_id","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityMfaMethodOktaRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Delete a configuration for the given MFA method","operationId":"deleteIdentityMfaMethodOktaMethod_id","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/mfa/method/pingid":{"get":{"summary":"List MFA method configurations for the given MFA method","operationId":"getIdentityMfaMethodPingid","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/mfa/method/pingid/{method_id}":{"parameters":[{"name":"method_id","description":"The unique identifier for this MFA method.","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Read the current configuration for the given MFA method","operationId":"getIdentityMfaMethodPingidMethod_id","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Update or create a configuration for the given MFA method","operationId":"postIdentityMfaMethodPingidMethod_id","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityMfaMethodPingidRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Delete a configuration for the given MFA method","operationId":"deleteIdentityMfaMethodPingidMethod_id","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/mfa/method/totp":{"get":{"summary":"List MFA method configurations for the given MFA method","operationId":"getIdentityMfaMethodTotp","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/mfa/method/totp/admin-destroy":{"post":{"summary":"Destroys a TOTP secret for the given MFA method ID on the given entity","operationId":"postIdentityMfaMethodTotpAdminDestroy","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityMfaMethodTotpAdminDestroyRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/mfa/method/totp/admin-generate":{"post":{"summary":"Update or create TOTP secret for the given method ID on the given entity.","operationId":"postIdentityMfaMethodTotpAdminGenerate","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityMfaMethodTotpAdminGenerateRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/mfa/method/totp/generate":{"post":{"summary":"Update or create TOTP secret for the given method ID on the given entity.","operationId":"postIdentityMfaMethodTotpGenerate","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityMfaMethodTotpGenerateRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/mfa/method/totp/{method_id}":{"parameters":[{"name":"method_id","description":"The unique identifier for this MFA method.","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Read the current configuration for the given MFA method","operationId":"getIdentityMfaMethodTotpMethod_id","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Update or create a configuration for the given MFA method","operationId":"postIdentityMfaMethodTotpMethod_id","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityMfaMethodTotpRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Delete a configuration for the given MFA method","operationId":"deleteIdentityMfaMethodTotpMethod_id","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/mfa/method/{method_id}":{"parameters":[{"name":"method_id","description":"The unique identifier for this MFA method.","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Read the current configuration for the given ID regardless of the MFA method type","operationId":"getIdentityMfaMethodMethod_id","tags":["identity"],"responses":{"200":{"description":"OK"}}}},"/identity/oidc/.well-known/keys":{"description":"Retrieve public keys","x-vault-unauthenticated":true,"get":{"summary":"Retrieve public keys","operationId":"getIdentityOidcWellKnownKeys","tags":["identity"],"responses":{"200":{"description":"OK"}}}},"/identity/oidc/.well-known/openid-configuration":{"description":"Query OIDC configurations","x-vault-unauthenticated":true,"get":{"summary":"Query OIDC configurations","operationId":"getIdentityOidcWellKnownOpenidConfiguration","tags":["identity"],"responses":{"200":{"description":"OK"}}}},"/identity/oidc/assignment":{"description":"List OIDC assignments","get":{"operationId":"getIdentityOidcAssignment","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/oidc/assignment/{name}":{"description":"CRUD operations for OIDC assignments.","parameters":[{"name":"name","description":"Name of the assignment","in":"path","schema":{"type":"string"},"required":true}],"x-vault-createSupported":true,"get":{"operationId":"getIdentityOidcAssignmentName","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"operationId":"postIdentityOidcAssignmentName","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityOidcAssignmentRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"operationId":"deleteIdentityOidcAssignmentName","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/oidc/client":{"description":"List OIDC clients","get":{"operationId":"getIdentityOidcClient","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/oidc/client/{name}":{"description":"CRUD operations for OIDC clients.","parameters":[{"name":"name","description":"Name of the client.","in":"path","schema":{"type":"string"},"required":true}],"x-vault-createSupported":true,"get":{"operationId":"getIdentityOidcClientName","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"operationId":"postIdentityOidcClientName","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityOidcClientRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"operationId":"deleteIdentityOidcClientName","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/oidc/config":{"description":"OIDC configuration","get":{"summary":"OIDC configuration","operationId":"getIdentityOidcConfig","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"OIDC configuration","operationId":"postIdentityOidcConfig","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityOidcConfigRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/oidc/introspect":{"description":"Verify the authenticity of an OIDC token","post":{"summary":"Verify the authenticity of an OIDC token","operationId":"postIdentityOidcIntrospect","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityOidcIntrospectRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/oidc/key":{"description":"List OIDC keys","get":{"summary":"List OIDC keys","operationId":"getIdentityOidcKey","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/oidc/key/{name}":{"description":"CRUD operations for OIDC keys.","parameters":[{"name":"name","description":"Name of the key","in":"path","schema":{"type":"string"},"required":true}],"x-vault-createSupported":true,"get":{"summary":"CRUD operations for OIDC keys.","operationId":"getIdentityOidcKeyName","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"CRUD operations for OIDC keys.","operationId":"postIdentityOidcKeyName","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityOidcKeyRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"CRUD operations for OIDC keys.","operationId":"deleteIdentityOidcKeyName","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/oidc/key/{name}/rotate":{"description":"Rotate a named OIDC key.","parameters":[{"name":"name","description":"Name of the key","in":"path","schema":{"type":"string"},"required":true}],"post":{"summary":"Rotate a named OIDC key.","operationId":"postIdentityOidcKeyNameRotate","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityOidcKeyRotateRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/oidc/provider":{"description":"List OIDC providers","parameters":[{"name":"allowed_client_id","description":"Filters the list of OIDC providers to those that allow the given client ID in their set of allowed_client_ids.","in":"query","schema":{"type":"string","default":""}}],"get":{"operationId":"getIdentityOidcProvider","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/oidc/provider/{name}":{"description":"CRUD operations for OIDC providers.","parameters":[{"name":"name","description":"Name of the provider","in":"path","schema":{"type":"string"},"required":true}],"x-vault-createSupported":true,"get":{"operationId":"getIdentityOidcProviderName","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"operationId":"postIdentityOidcProviderName","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityOidcProviderRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"operationId":"deleteIdentityOidcProviderName","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/oidc/provider/{name}/.well-known/keys":{"description":"Retrieve public keys","parameters":[{"name":"name","description":"Name of the provider","in":"path","schema":{"type":"string"},"required":true}],"get":{"operationId":"getIdentityOidcProviderNameWellKnownKeys","tags":["identity"],"responses":{"200":{"description":"OK"}}}},"/identity/oidc/provider/{name}/.well-known/openid-configuration":{"description":"Query OIDC configurations","parameters":[{"name":"name","description":"Name of the provider","in":"path","schema":{"type":"string"},"required":true}],"get":{"operationId":"getIdentityOidcProviderNameWellKnownOpenidConfiguration","tags":["identity"],"responses":{"200":{"description":"OK"}}}},"/identity/oidc/provider/{name}/authorize":{"description":"Provides the OIDC Authorization Endpoint.","parameters":[{"name":"name","description":"Name of the provider","in":"path","schema":{"type":"string"},"required":true}],"get":{"operationId":"getIdentityOidcProviderNameAuthorize","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"operationId":"postIdentityOidcProviderNameAuthorize","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityOidcProviderAuthorizeRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/oidc/provider/{name}/token":{"description":"Provides the OIDC Token Endpoint.","parameters":[{"name":"name","description":"Name of the provider","in":"path","schema":{"type":"string"},"required":true}],"post":{"operationId":"postIdentityOidcProviderNameToken","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityOidcProviderTokenRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/oidc/provider/{name}/userinfo":{"description":"Provides the OIDC UserInfo Endpoint.","parameters":[{"name":"name","description":"Name of the provider","in":"path","schema":{"type":"string"},"required":true}],"get":{"operationId":"getIdentityOidcProviderNameUserinfo","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"operationId":"postIdentityOidcProviderNameUserinfo","tags":["identity"],"responses":{"200":{"description":"OK"}}}},"/identity/oidc/role":{"description":"List configured OIDC roles","get":{"summary":"List configured OIDC roles","operationId":"getIdentityOidcRole","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/oidc/role/{name}":{"description":"CRUD operations on OIDC Roles","parameters":[{"name":"name","description":"Name of the role","in":"path","schema":{"type":"string"},"required":true}],"x-vault-createSupported":true,"get":{"summary":"CRUD operations on OIDC Roles","operationId":"getIdentityOidcRoleName","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"CRUD operations on OIDC Roles","operationId":"postIdentityOidcRoleName","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityOidcRoleRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"CRUD operations on OIDC Roles","operationId":"deleteIdentityOidcRoleName","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/oidc/scope":{"description":"List OIDC scopes","get":{"operationId":"getIdentityOidcScope","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/oidc/scope/{name}":{"description":"CRUD operations for OIDC scopes.","parameters":[{"name":"name","description":"Name of the scope","in":"path","schema":{"type":"string"},"required":true}],"x-vault-createSupported":true,"get":{"operationId":"getIdentityOidcScopeName","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"operationId":"postIdentityOidcScopeName","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityOidcScopeRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"operationId":"deleteIdentityOidcScopeName","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/identity/oidc/token/{name}":{"description":"Generate an OIDC token","parameters":[{"name":"name","description":"Name of the role","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Generate an OIDC token","operationId":"getIdentityOidcTokenName","tags":["identity"],"responses":{"200":{"description":"OK"}}}},"/identity/persona":{"description":"Create a new alias.","post":{"summary":"Create a new alias.","operationId":"postIdentityPersona","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityPersonaRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/identity/persona/id":{"description":"List all the alias IDs.","get":{"summary":"List all the alias IDs.","operationId":"getIdentityPersonaId","tags":["identity"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/identity/persona/id/{id}":{"description":"Update, read or delete an alias ID.","parameters":[{"name":"id","description":"ID of the persona","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Update, read or delete an alias ID.","operationId":"getIdentityPersonaIdId","tags":["identity"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Update, read or delete an alias ID.","operationId":"postIdentityPersonaIdId","tags":["identity"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityPersonaIdRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Update, read or delete an alias ID.","operationId":"deleteIdentityPersonaIdId","tags":["identity"],"responses":{"204":{"description":"empty body"}}}},"/secret/.*":{},"/secret/config":{"description":"Configures settings for the KV store","x-vault-createSupported":true,"get":{"summary":"Read the backend level settings.","operationId":"getSecretConfig","tags":["secrets"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Configure backend level settings that are applied to every key in the key-value store.","operationId":"postSecretConfig","tags":["secrets"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KvConfigRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/secret/data/{path}":{"description":"Write, Patch, Read, and Delete data in the Key-Value Store.","parameters":[{"name":"path","description":"Location of the secret.","in":"path","schema":{"type":"string"},"required":true}],"x-vault-createSupported":true,"get":{"summary":"Write, Patch, Read, and Delete data in the Key-Value Store.","operationId":"getSecretDataPath","tags":["secrets"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Write, Patch, Read, and Delete data in the Key-Value Store.","operationId":"postSecretDataPath","tags":["secrets"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KvDataRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Write, Patch, Read, and Delete data in the Key-Value Store.","operationId":"deleteSecretDataPath","tags":["secrets"],"responses":{"204":{"description":"empty body"}}}},"/secret/delete/{path}":{"description":"Marks one or more versions as deleted in the KV store.","parameters":[{"name":"path","description":"Location of the secret.","in":"path","schema":{"type":"string"},"required":true}],"x-vault-createSupported":true,"post":{"summary":"Marks one or more versions as deleted in the KV store.","operationId":"postSecretDeletePath","tags":["secrets"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KvDeleteRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/secret/destroy/{path}":{"description":"Permanently removes one or more versions in the KV store","parameters":[{"name":"path","description":"Location of the secret.","in":"path","schema":{"type":"string"},"required":true}],"x-vault-createSupported":true,"post":{"summary":"Permanently removes one or more versions in the KV store","operationId":"postSecretDestroyPath","tags":["secrets"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KvDestroyRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/secret/metadata/{path}":{"description":"Configures settings for the KV store","parameters":[{"name":"path","description":"Location of the secret.","in":"path","schema":{"type":"string"},"required":true}],"x-vault-createSupported":true,"get":{"summary":"Configures settings for the KV store","operationId":"getSecretMetadataPath","tags":["secrets"],"parameters":[{"name":"list","description":"Return a list if `true`","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Configures settings for the KV store","operationId":"postSecretMetadataPath","tags":["secrets"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KvMetadataRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Configures settings for the KV store","operationId":"deleteSecretMetadataPath","tags":["secrets"],"responses":{"204":{"description":"empty body"}}}},"/secret/subkeys/{path}":{"description":"Read the structure of a secret entry from the Key-Value store with the values removed.","parameters":[{"name":"path","description":"Location of the secret.","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Read the structure of a secret entry from the Key-Value store with the values removed.","operationId":"getSecretSubkeysPath","tags":["secrets"],"responses":{"200":{"description":"OK"}}}},"/secret/undelete/{path}":{"description":"Undeletes one or more versions from the KV store.","parameters":[{"name":"path","description":"Location of the secret.","in":"path","schema":{"type":"string"},"required":true}],"x-vault-createSupported":true,"post":{"summary":"Undeletes one or more versions from the KV store.","operationId":"postSecretUndeletePath","tags":["secrets"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KvUndeleteRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/audit":{"description":"List the currently enabled audit backends.","x-vault-sudo":true,"get":{"summary":"List the enabled audit devices.","operationId":"getSysAudit","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/audit-hash/{path}":{"description":"The hash of the given string via the given audit backend","parameters":[{"name":"path","description":"The name of the backend. Cannot be delimited. Example: \"mysql\"","in":"path","schema":{"type":"string"},"required":true}],"post":{"summary":"The hash of the given string via the given audit backend","operationId":"postSysAuditHashPath","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemAuditHashRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/audit/{path}":{"description":"Enable or disable audit backends.","parameters":[{"name":"path","description":"The name of the backend. Cannot be delimited. Example: \"mysql\"","in":"path","schema":{"type":"string"},"required":true}],"x-vault-sudo":true,"post":{"summary":"Enable a new audit device at the supplied path.","operationId":"postSysAuditPath","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemAuditRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Disable the audit device at the given path.","operationId":"deleteSysAuditPath","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/auth":{"description":"List the currently enabled credential backends.","get":{"summary":"List the currently enabled credential backends.","operationId":"getSysAuth","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/auth/{path}":{"description":"Enable a new credential backend with a name.","parameters":[{"name":"path","description":"The path to mount to. Cannot be delimited. Example: \"user\"","in":"path","schema":{"type":"string"},"required":true}],"x-vault-sudo":true,"get":{"summary":"Read the configuration of the auth engine at the given path.","operationId":"getSysAuthPath","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Enables a new auth method.","description":"After enabling, the auth method can be accessed and configured via the auth path specified as part of the URL. This auth path will be nested under the auth prefix.\n\nFor example, enable the \"foo\" auth method will make it accessible at /auth/foo.","operationId":"postSysAuthPath","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemAuthRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Disable the auth method at the given auth path","operationId":"deleteSysAuthPath","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/auth/{path}/tune":{"description":"Tune the configuration parameters for an auth path.","parameters":[{"name":"path","description":"Tune the configuration parameters for an auth path.","in":"path","schema":{"type":"string"},"required":true}],"x-vault-sudo":true,"get":{"summary":"Reads the given auth path's configuration.","description":"This endpoint requires sudo capability on the final path, but the same functionality can be achieved without sudo via `sys/mounts/auth/[auth-path]/tune`.","operationId":"getSysAuthPathTune","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Tune configuration parameters for a given auth path.","description":"This endpoint requires sudo capability on the final path, but the same functionality can be achieved without sudo via `sys/mounts/auth/[auth-path]/tune`.","operationId":"postSysAuthPathTune","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemAuthTuneRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/capabilities":{"description":"Fetches the capabilities of the given token on the given path.","post":{"summary":"Fetches the capabilities of the given token on the given path.","operationId":"postSysCapabilities","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemCapabilitiesRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/capabilities-accessor":{"description":"Fetches the capabilities of the token associated with the given token, on the given path.","post":{"summary":"Fetches the capabilities of the token associated with the given token, on the given path.","operationId":"postSysCapabilitiesAccessor","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemCapabilitiesAccessorRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/capabilities-self":{"description":"Fetches the capabilities of the given token on the given path.","post":{"summary":"Fetches the capabilities of the given token on the given path.","operationId":"postSysCapabilitiesSelf","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemCapabilitiesSelfRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/config/auditing/request-headers":{"description":"Lists the headers configured to be audited.","x-vault-sudo":true,"get":{"summary":"List the request headers that are configured to be audited.","operationId":"getSysConfigAuditingRequestHeaders","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/config/auditing/request-headers/{header}":{"description":"Configures the headers sent to the audit logs.","parameters":[{"name":"header","in":"path","schema":{"type":"string"},"required":true}],"x-vault-sudo":true,"get":{"summary":"List the information for the given request header.","operationId":"getSysConfigAuditingRequestHeadersHeader","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Enable auditing of a header.","operationId":"postSysConfigAuditingRequestHeadersHeader","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigAuditingRequestHeadersRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Disable auditing of the given request header.","operationId":"deleteSysConfigAuditingRequestHeadersHeader","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/config/cors":{"description":"This path responds to the following HTTP methods. GET / Returns the configuration of the CORS setting. POST / Sets the comma-separated list of origins that can make cross-origin requests. DELETE / Clears the CORS configuration and disables acceptance of CORS requests.","x-vault-sudo":true,"get":{"summary":"Return the current CORS settings.","operationId":"getSysConfigCors","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Configure the CORS settings.","operationId":"postSysConfigCors","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigCorsRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Remove any CORS settings.","operationId":"deleteSysConfigCors","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/config/reload/{subsystem}":{"parameters":[{"name":"subsystem","in":"path","schema":{"type":"string"},"required":true}],"post":{"summary":"Reload the given subsystem","operationId":"postSysConfigReloadSubsystem","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/config/state/sanitized":{"get":{"summary":"Return a sanitized version of the Vault server configuration.","description":"The sanitized output strips configuration values in the storage, HA storage, and seals stanzas, which may contain sensitive values such as API tokens. It also removes any token or secret fields in other stanzas, such as the circonus_api_token from telemetry.","operationId":"getSysConfigStateSanitized","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/config/ui/headers/":{"description":"This path responds to the following HTTP methods. GET /\u003cheader\u003e Returns the header value. POST /\u003cheader\u003e Sets the header value for the UI. DELETE /\u003cheader\u003e Clears the header value for UI. LIST / List the headers configured for the UI.","x-vault-sudo":true,"get":{"summary":"Return a list of configured UI headers.","operationId":"getSysConfigUiHeaders","tags":["system"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/sys/config/ui/headers/{header}":{"description":"This path responds to the following HTTP methods. GET /\u003cheader\u003e Returns the header value. POST /\u003cheader\u003e Sets the header value for the UI. DELETE /\u003cheader\u003e Clears the header value for UI. LIST / List the headers configured for the UI.","parameters":[{"name":"header","description":"The name of the header.","in":"path","schema":{"type":"string"},"required":true}],"x-vault-sudo":true,"get":{"summary":"Return the given UI header's configuration","operationId":"getSysConfigUiHeadersHeader","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Configure the values to be returned for the UI header.","operationId":"postSysConfigUiHeadersHeader","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigUiHeadersRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Remove a UI header.","operationId":"deleteSysConfigUiHeadersHeader","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/generate-root":{"description":"Reads, generates, or deletes a root token regeneration process.","get":{"summary":"Read the configuration and progress of the current root generation attempt.","operationId":"getSysGenerateRoot","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Initializes a new root generation attempt.","description":"Only a single root generation attempt can take place at a time. One (and only one) of otp or pgp_key are required.","operationId":"postSysGenerateRoot","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemGenerateRootRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Cancels any in-progress root generation attempt.","operationId":"deleteSysGenerateRoot","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/generate-root/attempt":{"description":"Reads, generates, or deletes a root token regeneration process.","x-vault-unauthenticated":true,"get":{"summary":"Read the configuration and progress of the current root generation attempt.","operationId":"getSysGenerateRootAttempt","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Initializes a new root generation attempt.","description":"Only a single root generation attempt can take place at a time. One (and only one) of otp or pgp_key are required.","operationId":"postSysGenerateRootAttempt","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemGenerateRootAttemptRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Cancels any in-progress root generation attempt.","operationId":"deleteSysGenerateRootAttempt","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/generate-root/update":{"description":"Reads, generates, or deletes a root token regeneration process.","x-vault-unauthenticated":true,"post":{"summary":"Enter a single unseal key share to progress the root generation attempt.","description":"If the threshold number of unseal key shares is reached, Vault will complete the root generation and issue the new token. Otherwise, this API must be called multiple times until that threshold is met. The attempt nonce must be provided with each call.","operationId":"postSysGenerateRootUpdate","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemGenerateRootUpdateRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/ha-status":{"description":"Provides information about the nodes in an HA cluster.","get":{"summary":"Check the HA status of a Vault cluster","operationId":"getSysHaStatus","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/health":{"description":"Checks the health status of the Vault.","x-vault-unauthenticated":true,"get":{"summary":"Returns the health status of Vault.","operationId":"getSysHealth","tags":["system"],"responses":{"200":{"description":"initialized, unsealed, and active"},"429":{"description":"unsealed and standby"},"472":{"description":"data recovery mode replication secondary and active"},"501":{"description":"not initialized"},"503":{"description":"sealed"}}}},"/sys/host-info":{"description":"Information about the host instance that this Vault server is running on.","get":{"summary":"Information about the host instance that this Vault server is running on.","description":"Information about the host instance that this Vault server is running on.\n\t\tThe information that gets collected includes host hardware information, and CPU,\n\t\tdisk, and memory utilization","operationId":"getSysHostInfo","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/in-flight-req":{"get":{"summary":"reports in-flight requests","description":"This path responds to the following HTTP methods.\n\t\tGET /\n\t\t\tReturns a map of in-flight requests.","operationId":"getSysInFlightReq","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/init":{"description":"Initializes or returns the initialization status of the Vault.","x-vault-unauthenticated":true,"get":{"summary":"Returns the initialization status of Vault.","operationId":"getSysInit","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Initialize a new Vault.","description":"The Vault must not have been previously initialized. The recovery options, as well as the stored shares option, are only available when using Vault HSM.","operationId":"postSysInit","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemInitRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/internal/counters/activity":{"description":"Query the historical count of clients.","get":{"summary":"Report the client count metrics, for this namespace and all child namespaces.","operationId":"getSysInternalCountersActivity","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/internal/counters/activity/export":{"description":"Export the historical activity of clients.","get":{"summary":"Report the client count metrics, for this namespace and all child namespaces.","operationId":"getSysInternalCountersActivityExport","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/internal/counters/activity/monthly":{"description":"Count of active clients so far this month.","get":{"summary":"Report the number of clients for this month, for this namespace and all child namespaces.","operationId":"getSysInternalCountersActivityMonthly","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/internal/counters/config":{"description":"Control the collection and reporting of client counts.","get":{"summary":"Read the client count tracking configuration.","operationId":"getSysInternalCountersConfig","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Enable or disable collection of client count, set retention period, or set default reporting period.","operationId":"postSysInternalCountersConfig","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemInternalCountersConfigRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/internal/counters/entities":{"description":"Count of active entities in this Vault cluster.","get":{"summary":"Backwards compatibility is not guaranteed for this API","operationId":"getSysInternalCountersEntities","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/internal/counters/requests":{"description":"Currently unsupported. Previously, count of requests seen by this Vault cluster over time.","get":{"summary":"Backwards compatibility is not guaranteed for this API","operationId":"getSysInternalCountersRequests","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/internal/counters/tokens":{"description":"Count of active tokens in this Vault cluster.","get":{"summary":"Backwards compatibility is not guaranteed for this API","operationId":"getSysInternalCountersTokens","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/internal/specs/openapi":{"x-vault-unauthenticated":true,"get":{"summary":"Generate an OpenAPI 3 document of all mounted paths.","operationId":"getSysInternalSpecsOpenapi","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/internal/ui/feature-flags":{"description":"Enabled feature flags. Internal API; its location, inputs, and outputs may change.","get":{"summary":"Lists enabled feature flags.","operationId":"getSysInternalUiFeatureFlags","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/internal/ui/mounts":{"description":"Information about mounts returned according to their tuned visibility. Internal API; its location, inputs, and outputs may change.","x-vault-unauthenticated":true,"get":{"summary":"Lists all enabled and visible auth and secrets mounts.","operationId":"getSysInternalUiMounts","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/internal/ui/mounts/{path}":{"description":"Information about mounts returned according to their tuned visibility. Internal API; its location, inputs, and outputs may change.","parameters":[{"name":"path","description":"The path of the mount.","in":"path","schema":{"type":"string"},"required":true}],"x-vault-unauthenticated":true,"get":{"summary":"Return information about the given mount.","operationId":"getSysInternalUiMountsPath","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/internal/ui/namespaces":{"description":"Information about visible child namespaces. Internal API; its location, inputs, and outputs may change.","x-vault-unauthenticated":true,"get":{"summary":"Backwards compatibility is not guaranteed for this API","operationId":"getSysInternalUiNamespaces","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/internal/ui/resultant-acl":{"description":"Information about a token's resultant ACL. Internal API; its location, inputs, and outputs may change.","get":{"summary":"Backwards compatibility is not guaranteed for this API","operationId":"getSysInternalUiResultantAcl","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/key-status":{"description":"Provides information about the backend encryption key.","get":{"summary":"Provides information about the backend encryption key.","operationId":"getSysKeyStatus","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/leader":{"description":"Check the high availability status and current leader of Vault","x-vault-unauthenticated":true,"get":{"summary":"Returns the high availability status and current leader instance of Vault.","operationId":"getSysLeader","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/leases":{"description":"List leases associated with this Vault cluster","x-vault-sudo":true,"get":{"summary":"List leases associated with this Vault cluster","operationId":"getSysLeases","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/leases/count":{"description":"Count of leases associated with this Vault cluster","get":{"summary":"Count of leases associated with this Vault cluster","operationId":"getSysLeasesCount","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/leases/lookup":{"description":"View or list lease metadata.","post":{"summary":"Retrieve lease metadata.","operationId":"postSysLeasesLookup","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemLeasesLookupRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/leases/lookup/":{"description":"View or list lease metadata.","x-vault-sudo":true,"get":{"summary":"Returns a list of lease ids.","operationId":"getSysLeasesLookup","tags":["system"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/sys/leases/lookup/{prefix}":{"description":"View or list lease metadata.","parameters":[{"name":"prefix","description":"The path to list leases under. Example: \"aws/creds/deploy\"","in":"path","schema":{"type":"string"},"required":true}],"x-vault-sudo":true,"get":{"summary":"Returns a list of lease ids.","operationId":"getSysLeasesLookupPrefix","tags":["system"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/sys/leases/renew":{"description":"Renew a lease on a secret","post":{"summary":"Renews a lease, requesting to extend the lease.","operationId":"postSysLeasesRenew","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemLeasesRenewRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/leases/renew/{url_lease_id}":{"description":"Renew a lease on a secret","parameters":[{"name":"url_lease_id","description":"The lease identifier to renew. This is included with a lease.","in":"path","schema":{"type":"string"},"required":true}],"post":{"summary":"Renews a lease, requesting to extend the lease.","operationId":"postSysLeasesRenewUrl_lease_id","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemLeasesRenewLeaseRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/leases/revoke":{"description":"Revoke a leased secret immediately","post":{"summary":"Revokes a lease immediately.","operationId":"postSysLeasesRevoke","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemLeasesRevokeRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/leases/revoke-force/{prefix}":{"description":"Revoke all secrets generated in a given prefix, ignoring errors.","parameters":[{"name":"prefix","description":"The path to revoke keys under. Example: \"prod/aws/ops\"","in":"path","schema":{"type":"string"},"required":true}],"x-vault-sudo":true,"post":{"summary":"Revokes all secrets or tokens generated under a given prefix immediately","description":"Unlike `/sys/leases/revoke-prefix`, this path ignores backend errors encountered during revocation. This is potentially very dangerous and should only be used in specific emergency situations where errors in the backend or the connected backend service prevent normal revocation.\n\nBy ignoring these errors, Vault abdicates responsibility for ensuring that the issued credentials or secrets are properly revoked and/or cleaned up. Access to this endpoint should be tightly controlled.","operationId":"postSysLeasesRevokeForcePrefix","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/leases/revoke-prefix/{prefix}":{"description":"Revoke all secrets generated in a given prefix","parameters":[{"name":"prefix","description":"The path to revoke keys under. Example: \"prod/aws/ops\"","in":"path","schema":{"type":"string"},"required":true}],"x-vault-sudo":true,"post":{"summary":"Revokes all secrets (via a lease ID prefix) or tokens (via the tokens' path property) generated under a given prefix immediately.","operationId":"postSysLeasesRevokePrefixPrefix","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemLeasesRevokePrefixRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/leases/revoke/{url_lease_id}":{"description":"Revoke a leased secret immediately","parameters":[{"name":"url_lease_id","description":"The lease identifier to renew. This is included with a lease.","in":"path","schema":{"type":"string"},"required":true}],"post":{"summary":"Revokes a lease immediately.","operationId":"postSysLeasesRevokeUrl_lease_id","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemLeasesRevokeLeaseRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/leases/tidy":{"description":"This endpoint performs cleanup tasks that can be run if certain error conditions have occurred.","post":{"summary":"This endpoint performs cleanup tasks that can be run if certain error\nconditions have occurred.","operationId":"postSysLeasesTidy","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/loggers":{"get":{"summary":"Read the log level for all existing loggers.","operationId":"getSysLoggers","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Modify the log level for all existing loggers.","operationId":"postSysLoggers","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemLoggersRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Revert the all loggers to use log level provided in config.","operationId":"deleteSysLoggers","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/loggers/{name}":{"parameters":[{"name":"name","description":"The name of the logger to be modified.","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Read the log level for a single logger.","operationId":"getSysLoggersName","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Modify the log level of a single logger.","operationId":"postSysLoggersName","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemLoggersRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Revert a single logger to use log level provided in config.","operationId":"deleteSysLoggersName","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/metrics":{"description":"Export the metrics aggregated for telemetry purpose.","parameters":[{"name":"format","description":"Format to export metrics into. Currently accepts only \"prometheus\".","in":"query","schema":{"type":"string"}}],"get":{"summary":"Export the metrics aggregated for telemetry purpose.","operationId":"getSysMetrics","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/mfa/validate":{"x-vault-unauthenticated":true,"post":{"summary":"Validates the login for the given MFA methods. Upon successful validation, it returns an auth response containing the client token","operationId":"postSysMfaValidate","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemMfaValidateRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/monitor":{"parameters":[{"name":"log_format","description":"Output format of logs. Supported values are \"standard\" and \"json\". The default is \"standard\".","in":"query","schema":{"type":"string","default":"standard"}},{"name":"log_level","description":"Log level to view system logs at. Currently supported values are \"trace\", \"debug\", \"info\", \"warn\", \"error\".","in":"query","schema":{"type":"string"}}],"get":{"operationId":"getSysMonitor","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/mounts":{"description":"List the currently mounted backends.","get":{"summary":"List the currently mounted backends.","operationId":"getSysMounts","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/mounts/{path}":{"description":"Mount a new backend at a new path.","parameters":[{"name":"path","description":"The path to mount to. Example: \"aws/east\"","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Read the configuration of the secret engine at the given path.","operationId":"getSysMountsPath","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Enable a new secrets engine at the given path.","operationId":"postSysMountsPath","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemMountsRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Disable the mount point specified at the given path.","operationId":"deleteSysMountsPath","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/mounts/{path}/tune":{"description":"Tune backend configuration parameters for this mount.","parameters":[{"name":"path","description":"The path to mount to. Example: \"aws/east\"","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Tune backend configuration parameters for this mount.","operationId":"getSysMountsPathTune","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Tune backend configuration parameters for this mount.","operationId":"postSysMountsPathTune","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemMountsTuneRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/plugins/catalog":{"description":"Lists all the plugins known to Vault","get":{"summary":"Lists all the plugins known to Vault","operationId":"getSysPluginsCatalog","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/plugins/catalog/{name}":{"description":"Configures the plugins known to Vault","parameters":[{"name":"name","description":"The name of the plugin","in":"path","schema":{"type":"string"},"required":true}],"x-vault-sudo":true,"get":{"summary":"Return the configuration data for the plugin with the given name.","operationId":"getSysPluginsCatalogName","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Register a new plugin, or updates an existing one with the supplied name.","operationId":"postSysPluginsCatalogName","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemPluginsCatalogRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Remove the plugin with the given name.","operationId":"deleteSysPluginsCatalogName","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/plugins/catalog/{type}":{"description":"Configures the plugins known to Vault","parameters":[{"name":"type","description":"The type of the plugin, may be auth, secret, or database","in":"path","schema":{"type":"string"},"required":true}],"x-vault-sudo":true,"get":{"summary":"List the plugins in the catalog.","operationId":"getSysPluginsCatalogType","tags":["system"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/sys/plugins/catalog/{type}/{name}":{"description":"Configures the plugins known to Vault","parameters":[{"name":"name","description":"The name of the plugin","in":"path","schema":{"type":"string"},"required":true},{"name":"type","description":"The type of the plugin, may be auth, secret, or database","in":"path","schema":{"type":"string"},"required":true}],"x-vault-sudo":true,"get":{"summary":"Return the configuration data for the plugin with the given name.","operationId":"getSysPluginsCatalogTypeName","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Register a new plugin, or updates an existing one with the supplied name.","operationId":"postSysPluginsCatalogTypeName","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemPluginsCatalogRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Remove the plugin with the given name.","operationId":"deleteSysPluginsCatalogTypeName","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/plugins/reload/backend":{"description":"Reload mounts that use a particular backend plugin.","post":{"summary":"Reload mounted plugin backends.","description":"Either the plugin name (`plugin`) or the desired plugin backend mounts (`mounts`) must be provided, but not both. In the case that the plugin name is provided, all mounted paths that use that plugin backend will be reloaded. If (`scope`) is provided and is (`global`), the plugin(s) are reloaded globally.","operationId":"postSysPluginsReloadBackend","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemPluginsReloadBackendRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/policies/acl":{"description":"List the configured access control policies.","get":{"summary":"List the configured access control policies.","operationId":"getSysPoliciesAcl","tags":["system"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/sys/policies/acl/{name}":{"description":"Read, Modify, or Delete an access control policy.","parameters":[{"name":"name","description":"The name of the policy. Example: \"ops\"","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Retrieve information about the named ACL policy.","operationId":"getSysPoliciesAclName","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Add a new or update an existing ACL policy.","operationId":"postSysPoliciesAclName","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemPoliciesAclRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Delete the ACL policy with the given name.","operationId":"deleteSysPoliciesAclName","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/policies/password":{"get":{"summary":"List the existing password policies.","operationId":"getSysPoliciesPassword","tags":["system"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/sys/policies/password/{name}":{"description":"Read, Modify, or Delete a password policy.","parameters":[{"name":"name","description":"The name of the password policy.","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Retrieve an existing password policy.","operationId":"getSysPoliciesPasswordName","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Add a new or update an existing password policy.","operationId":"postSysPoliciesPasswordName","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemPoliciesPasswordRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Delete a password policy.","operationId":"deleteSysPoliciesPasswordName","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/policies/password/{name}/generate":{"description":"Generate a password from an existing password policy.","parameters":[{"name":"name","description":"The name of the password policy.","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Generate a password from an existing password policy.","operationId":"getSysPoliciesPasswordNameGenerate","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/policy":{"description":"List the configured access control policies.","get":{"summary":"List the configured access control policies.","operationId":"getSysPolicy","tags":["system"],"parameters":[{"name":"list","description":"Return a list if `true`","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/sys/policy/{name}":{"description":"Read, Modify, or Delete an access control policy.","parameters":[{"name":"name","description":"The name of the policy. Example: \"ops\"","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Retrieve the policy body for the named policy.","operationId":"getSysPolicyName","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Add a new or update an existing policy.","operationId":"postSysPolicyName","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemPolicyRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Delete the policy with the given name.","operationId":"deleteSysPolicyName","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/pprof/":{"get":{"summary":"Returns an HTML page listing the available profiles.","description":"Returns an HTML page listing the available \nprofiles. This should be mainly accessed via browsers or applications that can \nrender pages.","operationId":"getSysPprof","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/pprof/allocs":{"get":{"summary":"Returns a sampling of all past memory allocations.","description":"Returns a sampling of all past memory allocations.","operationId":"getSysPprofAllocs","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/pprof/block":{"get":{"summary":"Returns stack traces that led to blocking on synchronization primitives","description":"Returns stack traces that led to blocking on synchronization primitives","operationId":"getSysPprofBlock","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/pprof/cmdline":{"get":{"summary":"Returns the running program's command line.","description":"Returns the running program's command line, with arguments separated by NUL bytes.","operationId":"getSysPprofCmdline","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/pprof/goroutine":{"get":{"summary":"Returns stack traces of all current goroutines.","description":"Returns stack traces of all current goroutines.","operationId":"getSysPprofGoroutine","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/pprof/heap":{"get":{"summary":"Returns a sampling of memory allocations of live object.","description":"Returns a sampling of memory allocations of live object.","operationId":"getSysPprofHeap","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/pprof/mutex":{"get":{"summary":"Returns stack traces of holders of contended mutexes","description":"Returns stack traces of holders of contended mutexes","operationId":"getSysPprofMutex","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/pprof/profile":{"get":{"summary":"Returns a pprof-formatted cpu profile payload.","description":"Returns a pprof-formatted cpu profile payload. Profiling lasts for duration specified in seconds GET parameter, or for 30 seconds if not specified.","operationId":"getSysPprofProfile","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/pprof/symbol":{"get":{"summary":"Returns the program counters listed in the request.","description":"Returns the program counters listed in the request.","operationId":"getSysPprofSymbol","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/pprof/threadcreate":{"get":{"summary":"Returns stack traces that led to the creation of new OS threads","description":"Returns stack traces that led to the creation of new OS threads","operationId":"getSysPprofThreadcreate","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/pprof/trace":{"get":{"summary":"Returns the execution trace in binary form.","description":"Returns the execution trace in binary form. Tracing lasts for duration specified in seconds GET parameter, or for 1 second if not specified.","operationId":"getSysPprofTrace","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/quotas/config":{"description":"Create, update and read the quota configuration.","get":{"operationId":"getSysQuotasConfig","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"operationId":"postSysQuotasConfig","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemQuotasConfigRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/quotas/rate-limit":{"description":"Lists the names of all the rate limit quotas.","get":{"operationId":"getSysQuotasRateLimit","tags":["system"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/sys/quotas/rate-limit/{name}":{"description":"Get, create or update rate limit resource quota for an optional namespace or mount.","parameters":[{"name":"name","description":"Name of the quota rule.","in":"path","schema":{"type":"string"},"required":true}],"get":{"operationId":"getSysQuotasRateLimitName","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"operationId":"postSysQuotasRateLimitName","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemQuotasRateLimitRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"operationId":"deleteSysQuotasRateLimitName","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/raw":{"description":"Write, Read, and Delete data directly in the Storage backend.","x-vault-sudo":true,"x-vault-createSupported":true,"get":{"summary":"Read the value of the key at the given path.","operationId":"getSysRaw","tags":["system"],"parameters":[{"name":"list","description":"Return a list if `true`","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Update the value of the key at the given path.","operationId":"postSysRaw","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemRawRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Delete the key with given path.","operationId":"deleteSysRaw","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/raw/{path}":{"description":"Write, Read, and Delete data directly in the Storage backend.","parameters":[{"name":"path","in":"path","schema":{"type":"string"},"required":true}],"x-vault-sudo":true,"x-vault-createSupported":true,"get":{"summary":"Read the value of the key at the given path.","operationId":"getSysRawPath","tags":["system"],"parameters":[{"name":"list","description":"Return a list if `true`","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Update the value of the key at the given path.","operationId":"postSysRawPath","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemRawRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Delete the key with given path.","operationId":"deleteSysRawPath","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/rekey/backup":{"description":"Allows fetching or deleting the backup of the rotated unseal keys.","get":{"summary":"Return the backup copy of PGP-encrypted unseal keys.","operationId":"getSysRekeyBackup","tags":["system"],"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Delete the backup copy of PGP-encrypted unseal keys.","operationId":"deleteSysRekeyBackup","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/rekey/init":{"x-vault-unauthenticated":true,"get":{"summary":"Reads the configuration and progress of the current rekey attempt.","operationId":"getSysRekeyInit","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Initializes a new rekey attempt.","description":"Only a single rekey attempt can take place at a time, and changing the parameters of a rekey requires canceling and starting a new rekey, which will also provide a new nonce.","operationId":"postSysRekeyInit","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemRekeyInitRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Cancels any in-progress rekey.","description":"This clears the rekey settings as well as any progress made. This must be called to change the parameters of the rekey. Note: verification is still a part of a rekey. If rekeying is canceled during the verification flow, the current unseal keys remain valid.","operationId":"deleteSysRekeyInit","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/rekey/recovery-key-backup":{"description":"Allows fetching or deleting the backup of the rotated unseal keys.","get":{"summary":"Allows fetching or deleting the backup of the rotated unseal keys.","operationId":"getSysRekeyRecoveryKeyBackup","tags":["system"],"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Allows fetching or deleting the backup of the rotated unseal keys.","operationId":"deleteSysRekeyRecoveryKeyBackup","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/rekey/update":{"x-vault-unauthenticated":true,"post":{"summary":"Enter a single unseal key share to progress the rekey of the Vault.","operationId":"postSysRekeyUpdate","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemRekeyUpdateRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/rekey/verify":{"x-vault-unauthenticated":true,"get":{"summary":"Read the configuration and progress of the current rekey verification attempt.","operationId":"getSysRekeyVerify","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Enter a single new key share to progress the rekey verification operation.","operationId":"postSysRekeyVerify","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemRekeyVerifyRequest"}}}},"responses":{"200":{"description":"OK"}}},"delete":{"summary":"Cancel any in-progress rekey verification operation.","description":"This clears any progress made and resets the nonce. Unlike a `DELETE` against `sys/rekey/init`, this only resets the current verification operation, not the entire rekey atttempt.","operationId":"deleteSysRekeyVerify","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/remount":{"description":"Move the mount point of an already-mounted backend, within or across namespaces","x-vault-sudo":true,"post":{"summary":"Initiate a mount migration","operationId":"postSysRemount","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemRemountRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/remount/status/{migration_id}":{"description":"Check the status of a mount move operation","parameters":[{"name":"migration_id","description":"The ID of the migration operation","in":"path","schema":{"type":"string"},"required":true}],"get":{"summary":"Check status of a mount migration","operationId":"getSysRemountStatusMigration_id","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/renew":{"description":"Renew a lease on a secret","post":{"summary":"Renews a lease, requesting to extend the lease.","operationId":"postSysRenew","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemRenewRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/renew/{url_lease_id}":{"description":"Renew a lease on a secret","parameters":[{"name":"url_lease_id","description":"The lease identifier to renew. This is included with a lease.","in":"path","schema":{"type":"string"},"required":true}],"post":{"summary":"Renews a lease, requesting to extend the lease.","operationId":"postSysRenewUrl_lease_id","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemRenewLeaseRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/replication/status":{"x-vault-unauthenticated":true,"get":{"operationId":"getSysReplicationStatus","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/revoke":{"description":"Revoke a leased secret immediately","post":{"summary":"Revokes a lease immediately.","operationId":"postSysRevoke","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemRevokeRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/revoke-force/{prefix}":{"description":"Revoke all secrets generated in a given prefix, ignoring errors.","parameters":[{"name":"prefix","description":"The path to revoke keys under. Example: \"prod/aws/ops\"","in":"path","schema":{"type":"string"},"required":true}],"x-vault-sudo":true,"post":{"summary":"Revokes all secrets or tokens generated under a given prefix immediately","description":"Unlike `/sys/leases/revoke-prefix`, this path ignores backend errors encountered during revocation. This is potentially very dangerous and should only be used in specific emergency situations where errors in the backend or the connected backend service prevent normal revocation.\n\nBy ignoring these errors, Vault abdicates responsibility for ensuring that the issued credentials or secrets are properly revoked and/or cleaned up. Access to this endpoint should be tightly controlled.","operationId":"postSysRevokeForcePrefix","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/revoke-prefix/{prefix}":{"description":"Revoke all secrets generated in a given prefix","parameters":[{"name":"prefix","description":"The path to revoke keys under. Example: \"prod/aws/ops\"","in":"path","schema":{"type":"string"},"required":true}],"x-vault-sudo":true,"post":{"summary":"Revokes all secrets (via a lease ID prefix) or tokens (via the tokens' path property) generated under a given prefix immediately.","operationId":"postSysRevokePrefixPrefix","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemRevokePrefixRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/revoke/{url_lease_id}":{"description":"Revoke a leased secret immediately","parameters":[{"name":"url_lease_id","description":"The lease identifier to renew. This is included with a lease.","in":"path","schema":{"type":"string"},"required":true}],"post":{"summary":"Revokes a lease immediately.","operationId":"postSysRevokeUrl_lease_id","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemRevokeLeaseRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/rotate":{"description":"Rotates the backend encryption key used to persist data.","x-vault-sudo":true,"post":{"summary":"Rotates the backend encryption key used to persist data.","operationId":"postSysRotate","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/rotate/config":{"description":"Configures settings related to the backend encryption key management.","get":{"operationId":"getSysRotateConfig","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"operationId":"postSysRotateConfig","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemRotateConfigRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/seal":{"description":"Seals the Vault.","post":{"summary":"Seal the Vault.","operationId":"postSysSeal","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/seal-status":{"description":"Returns the seal status of the Vault.","x-vault-unauthenticated":true,"get":{"summary":"Check the seal status of a Vault.","operationId":"getSysSealStatus","tags":["system"],"responses":{"200":{"description":"OK"}}}},"/sys/step-down":{"post":{"summary":"Cause the node to give up active status.","description":"This endpoint forces the node to give up active status. If the node does not have active status, this endpoint does nothing. Note that the node will sleep for ten seconds before attempting to grab the active lock again, but if no standby nodes grab the active lock in the interim, the same node may become the active node again.","operationId":"postSysStepDown","tags":["system"],"responses":{"204":{"description":"empty body"}}}},"/sys/tools/hash":{"description":"Generate a hash sum for input data","post":{"summary":"Generate a hash sum for input data","operationId":"postSysToolsHash","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemToolsHashRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/tools/hash/{urlalgorithm}":{"description":"Generate a hash sum for input data","parameters":[{"name":"urlalgorithm","description":"Algorithm to use (POST URL parameter)","in":"path","schema":{"type":"string"},"required":true}],"post":{"summary":"Generate a hash sum for input data","operationId":"postSysToolsHashUrlalgorithm","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemToolsHashRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/tools/random":{"description":"Generate random bytes","post":{"summary":"Generate random bytes","operationId":"postSysToolsRandom","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemToolsRandomRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/tools/random/{source}":{"description":"Generate random bytes","parameters":[{"name":"source","description":"Which system to source random data from, ether \"platform\", \"seal\", or \"all\".","in":"path","schema":{"type":"string","default":"platform"},"required":true}],"post":{"summary":"Generate random bytes","operationId":"postSysToolsRandomSource","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemToolsRandomRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/tools/random/{source}/{urlbytes}":{"description":"Generate random bytes","parameters":[{"name":"source","description":"Which system to source random data from, ether \"platform\", \"seal\", or \"all\".","in":"path","schema":{"type":"string","default":"platform"},"required":true},{"name":"urlbytes","description":"The number of bytes to generate (POST URL parameter)","in":"path","schema":{"type":"string"},"required":true}],"post":{"summary":"Generate random bytes","operationId":"postSysToolsRandomSourceUrlbytes","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemToolsRandomRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/tools/random/{urlbytes}":{"description":"Generate random bytes","parameters":[{"name":"urlbytes","description":"The number of bytes to generate (POST URL parameter)","in":"path","schema":{"type":"string"},"required":true}],"post":{"summary":"Generate random bytes","operationId":"postSysToolsRandomUrlbytes","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemToolsRandomRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/unseal":{"description":"Unseals the Vault.","x-vault-unauthenticated":true,"post":{"summary":"Unseal the Vault.","operationId":"postSysUnseal","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemUnsealRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/version-history/":{"description":"List historical version changes sorted by installation time in ascending order.","get":{"summary":"Returns map of historical version change entries","operationId":"getSysVersionHistory","tags":["system"],"parameters":[{"name":"list","description":"Must be set to `true`","in":"query","schema":{"type":"string","enum":["true"]},"required":true}],"responses":{"200":{"description":"OK"}}}},"/sys/wrapping/lookup":{"description":"Looks up the properties of a response-wrapped token.","x-vault-unauthenticated":true,"get":{"summary":"Look up wrapping properties for the requester's token.","operationId":"getSysWrappingLookup","tags":["system"],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Look up wrapping properties for the given token.","operationId":"postSysWrappingLookup","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemWrappingLookupRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/wrapping/rewrap":{"description":"Rotates a response-wrapped token.","post":{"summary":"Rotates a response-wrapped token.","operationId":"postSysWrappingRewrap","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemWrappingRewrapRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/wrapping/unwrap":{"description":"Unwraps a response-wrapped token.","post":{"summary":"Unwraps a response-wrapped token.","operationId":"postSysWrappingUnwrap","tags":["system"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemWrappingUnwrapRequest"}}}},"responses":{"200":{"description":"OK"}}}},"/sys/wrapping/wrap":{"description":"Response-wraps an arbitrary JSON object.","post":{"summary":"Response-wraps an arbitrary JSON object.","operationId":"postSysWrappingWrap","tags":["system"],"responses":{"200":{"description":"OK"}}}}},"components":{"schemas":{"IdentityAliasIdRequest":{"type":"object","properties":{"canonical_id":{"type":"string","description":"Entity ID to which this alias should be tied to"},"entity_id":{"type":"string","description":"Entity ID to which this alias should be tied to. This field is deprecated in favor of 'canonical_id'."},"mount_accessor":{"type":"string","description":"Mount accessor to which this alias belongs to"},"name":{"type":"string","description":"Name of the alias"}}},"IdentityAliasRequest":{"type":"object","properties":{"canonical_id":{"type":"string","description":"Entity ID to which this alias belongs to"},"entity_id":{"type":"string","description":"Entity ID to which this alias belongs to. This field is deprecated in favor of 'canonical_id'."},"id":{"type":"string","description":"ID of the alias"},"mount_accessor":{"type":"string","description":"Mount accessor to which this alias belongs to"},"name":{"type":"string","description":"Name of the alias"}}},"IdentityEntityAliasIdRequest":{"type":"object","properties":{"canonical_id":{"type":"string","description":"Entity ID to which this alias should be tied to"},"custom_metadata":{"type":"object","description":"User provided key-value pairs","format":"kvpairs"},"entity_id":{"type":"string","description":"Entity ID to which this alias belongs to. This field is deprecated, use canonical_id."},"mount_accessor":{"type":"string","description":"(Unused)"},"name":{"type":"string","description":"(Unused)"}}},"IdentityEntityAliasRequest":{"type":"object","properties":{"canonical_id":{"type":"string","description":"Entity ID to which this alias belongs"},"custom_metadata":{"type":"object","description":"User provided key-value pairs","format":"kvpairs"},"entity_id":{"type":"string","description":"Entity ID to which this alias belongs. This field is deprecated, use canonical_id."},"id":{"type":"string","description":"ID of the entity alias. If set, updates the corresponding entity alias."},"mount_accessor":{"type":"string","description":"Mount accessor to which this alias belongs to; unused for a modify"},"name":{"type":"string","description":"Name of the alias; unused for a modify"}}},"IdentityEntityBatchDeleteRequest":{"type":"object","properties":{"entity_ids":{"type":"array","description":"Entity IDs to delete","items":{"type":"string"}}}},"IdentityEntityIdRequest":{"type":"object","properties":{"disabled":{"type":"boolean","description":"If set true, tokens tied to this identity will not be able to be used (but will not be revoked)."},"metadata":{"type":"object","description":"Metadata to be associated with the entity. In CLI, this parameter can be repeated multiple times, and it all gets merged together. For example: vault \u003ccommand\u003e \u003cpath\u003e metadata=key1=value1 metadata=key2=value2","format":"kvpairs"},"name":{"type":"string","description":"Name of the entity"},"policies":{"type":"array","description":"Policies to be tied to the entity.","items":{"type":"string"}}}},"IdentityEntityMergeRequest":{"type":"object","properties":{"conflicting_alias_ids_to_keep":{"type":"array","description":"Alias IDs to keep in case of conflicting aliases. Ignored if no conflicting aliases found","items":{"type":"string"}},"force":{"type":"boolean","description":"Setting this will follow the 'mine' strategy for merging MFA secrets. If there are secrets of the same type both in entities that are merged from and in entity into which all others are getting merged, secrets in the destination will be unaltered. If not set, this API will throw an error containing all the conflicts."},"from_entity_ids":{"type":"array","description":"Entity IDs which need to get merged","items":{"type":"string"}},"to_entity_id":{"type":"string","description":"Entity ID into which all the other entities need to get merged"}}},"IdentityEntityNameRequest":{"type":"object","properties":{"disabled":{"type":"boolean","description":"If set true, tokens tied to this identity will not be able to be used (but will not be revoked)."},"id":{"type":"string","description":"ID of the entity. If set, updates the corresponding existing entity."},"metadata":{"type":"object","description":"Metadata to be associated with the entity. In CLI, this parameter can be repeated multiple times, and it all gets merged together. For example: vault \u003ccommand\u003e \u003cpath\u003e metadata=key1=value1 metadata=key2=value2","format":"kvpairs"},"policies":{"type":"array","description":"Policies to be tied to the entity.","items":{"type":"string"}}}},"IdentityEntityRequest":{"type":"object","properties":{"disabled":{"type":"boolean","description":"If set true, tokens tied to this identity will not be able to be used (but will not be revoked)."},"id":{"type":"string","description":"ID of the entity. If set, updates the corresponding existing entity."},"metadata":{"type":"object","description":"Metadata to be associated with the entity. In CLI, this parameter can be repeated multiple times, and it all gets merged together. For example: vault \u003ccommand\u003e \u003cpath\u003e metadata=key1=value1 metadata=key2=value2","format":"kvpairs"},"name":{"type":"string","description":"Name of the entity"},"policies":{"type":"array","description":"Policies to be tied to the entity.","items":{"type":"string"}}}},"IdentityGroupAliasIdRequest":{"type":"object","properties":{"canonical_id":{"type":"string","description":"ID of the group to which this is an alias."},"mount_accessor":{"type":"string","description":"Mount accessor to which this alias belongs to."},"name":{"type":"string","description":"Alias of the group."}}},"IdentityGroupAliasRequest":{"type":"object","properties":{"canonical_id":{"type":"string","description":"ID of the group to which this is an alias."},"id":{"type":"string","description":"ID of the group alias."},"mount_accessor":{"type":"string","description":"Mount accessor to which this alias belongs to."},"name":{"type":"string","description":"Alias of the group."}}},"IdentityGroupIdRequest":{"type":"object","properties":{"member_entity_ids":{"type":"array","description":"Entity IDs to be assigned as group members.","items":{"type":"string"}},"member_group_ids":{"type":"array","description":"Group IDs to be assigned as group members.","items":{"type":"string"}},"metadata":{"type":"object","description":"Metadata to be associated with the group. In CLI, this parameter can be repeated multiple times, and it all gets merged together. For example: vault \u003ccommand\u003e \u003cpath\u003e metadata=key1=value1 metadata=key2=value2","format":"kvpairs"},"name":{"type":"string","description":"Name of the group."},"policies":{"type":"array","description":"Policies to be tied to the group.","items":{"type":"string"}},"type":{"type":"string","description":"Type of the group, 'internal' or 'external'. Defaults to 'internal'"}}},"IdentityGroupNameRequest":{"type":"object","properties":{"id":{"type":"string","description":"ID of the group. If set, updates the corresponding existing group."},"member_entity_ids":{"type":"array","description":"Entity IDs to be assigned as group members.","items":{"type":"string"}},"member_group_ids":{"type":"array","description":"Group IDs to be assigned as group members.","items":{"type":"string"}},"metadata":{"type":"object","description":"Metadata to be associated with the group. In CLI, this parameter can be repeated multiple times, and it all gets merged together. For example: vault \u003ccommand\u003e \u003cpath\u003e metadata=key1=value1 metadata=key2=value2","format":"kvpairs"},"policies":{"type":"array","description":"Policies to be tied to the group.","items":{"type":"string"}},"type":{"type":"string","description":"Type of the group, 'internal' or 'external'. Defaults to 'internal'"}}},"IdentityGroupRequest":{"type":"object","properties":{"id":{"type":"string","description":"ID of the group. If set, updates the corresponding existing group."},"member_entity_ids":{"type":"array","description":"Entity IDs to be assigned as group members.","items":{"type":"string"}},"member_group_ids":{"type":"array","description":"Group IDs to be assigned as group members.","items":{"type":"string"}},"metadata":{"type":"object","description":"Metadata to be associated with the group. In CLI, this parameter can be repeated multiple times, and it all gets merged together. For example: vault \u003ccommand\u003e \u003cpath\u003e metadata=key1=value1 metadata=key2=value2","format":"kvpairs"},"name":{"type":"string","description":"Name of the group."},"policies":{"type":"array","description":"Policies to be tied to the group.","items":{"type":"string"}},"type":{"type":"string","description":"Type of the group, 'internal' or 'external'. Defaults to 'internal'"}}},"IdentityLookupEntityRequest":{"type":"object","properties":{"alias_id":{"type":"string","description":"ID of the alias."},"alias_mount_accessor":{"type":"string","description":"Accessor of the mount to which the alias belongs to. This should be supplied in conjunction with 'alias_name'."},"alias_name":{"type":"string","description":"Name of the alias. This should be supplied in conjunction with 'alias_mount_accessor'."},"id":{"type":"string","description":"ID of the entity."},"name":{"type":"string","description":"Name of the entity."}}},"IdentityLookupGroupRequest":{"type":"object","properties":{"alias_id":{"type":"string","description":"ID of the alias."},"alias_mount_accessor":{"type":"string","description":"Accessor of the mount to which the alias belongs to. This should be supplied in conjunction with 'alias_name'."},"alias_name":{"type":"string","description":"Name of the alias. This should be supplied in conjunction with 'alias_mount_accessor'."},"id":{"type":"string","description":"ID of the group."},"name":{"type":"string","description":"Name of the group."}}},"IdentityMfaLoginEnforcementRequest":{"type":"object","properties":{"auth_method_accessors":{"type":"array","description":"Array of auth mount accessor IDs","items":{"type":"string"}},"auth_method_types":{"type":"array","description":"Array of auth mount types","items":{"type":"string"}},"identity_entity_ids":{"type":"array","description":"Array of identity entity IDs","items":{"type":"string"}},"identity_group_ids":{"type":"array","description":"Array of identity group IDs","items":{"type":"string"}},"mfa_method_ids":{"type":"array","description":"Array of Method IDs that determine what methods will be enforced","items":{"type":"string"}}},"required":["mfa_method_ids"]},"IdentityMfaMethodDuoRequest":{"type":"object","properties":{"api_hostname":{"type":"string","description":"API host name for Duo."},"integration_key":{"type":"string","description":"Integration key for Duo."},"method_id":{"type":"string","description":"The unique identifier for this MFA method."},"push_info":{"type":"string","description":"Push information for Duo."},"secret_key":{"type":"string","description":"Secret key for Duo."},"use_passcode":{"type":"boolean","description":"If true, the user is reminded to use the passcode upon MFA validation. This option does not enforce using the passcode. Defaults to false."},"username_format":{"type":"string","description":"A template string for mapping Identity names to MFA method names. Values to subtitute should be placed in {{}}. For example, \"{{alias.name}}@example.com\". Currently-supported mappings: alias.name: The name returned by the mount configured via the mount_accessor parameter If blank, the Alias's name field will be used as-is."}}},"IdentityMfaMethodOktaRequest":{"type":"object","properties":{"api_token":{"type":"string","description":"Okta API key."},"base_url":{"type":"string","description":"The base domain to use for the Okta API. When not specified in the configuration, \"okta.com\" is used."},"method_id":{"type":"string","description":"The unique identifier for this MFA method."},"org_name":{"type":"string","description":"Name of the organization to be used in the Okta API."},"primary_email":{"type":"boolean","description":"If true, the username will only match the primary email for the account. Defaults to false."},"production":{"type":"boolean","description":"(DEPRECATED) Use base_url instead."},"username_format":{"type":"string","description":"A template string for mapping Identity names to MFA method names. Values to substitute should be placed in {{}}. For example, \"{{entity.name}}@example.com\". If blank, the Entity's name field will be used as-is."}}},"IdentityMfaMethodPingidRequest":{"type":"object","properties":{"method_id":{"type":"string","description":"The unique identifier for this MFA method."},"settings_file_base64":{"type":"string","description":"The settings file provided by Ping, Base64-encoded. This must be a settings file suitable for third-party clients, not the PingID SDK or PingFederate."},"username_format":{"type":"string","description":"A template string for mapping Identity names to MFA method names. Values to subtitute should be placed in {{}}. For example, \"{{alias.name}}@example.com\". Currently-supported mappings: alias.name: The name returned by the mount configured via the mount_accessor parameter If blank, the Alias's name field will be used as-is."}}},"IdentityMfaMethodTotpAdminDestroyRequest":{"type":"object","properties":{"entity_id":{"type":"string","description":"Identifier of the entity from which the MFA method secret needs to be removed."},"method_id":{"type":"string","description":"The unique identifier for this MFA method."}},"required":["method_id","entity_id"]},"IdentityMfaMethodTotpAdminGenerateRequest":{"type":"object","properties":{"entity_id":{"type":"string","description":"Entity ID on which the generated secret needs to get stored."},"method_id":{"type":"string","description":"The unique identifier for this MFA method."}},"required":["entity_id","method_id"]},"IdentityMfaMethodTotpGenerateRequest":{"type":"object","properties":{"method_id":{"type":"string","description":"The unique identifier for this MFA method."}},"required":["method_id"]},"IdentityMfaMethodTotpRequest":{"type":"object","properties":{"algorithm":{"type":"string","description":"The hashing algorithm used to generate the TOTP token. Options include SHA1, SHA256 and SHA512.","default":"SHA1"},"digits":{"type":"integer","description":"The number of digits in the generated TOTP token. This value can either be 6 or 8.","default":6},"issuer":{"type":"string","description":"The name of the key's issuing organization."},"key_size":{"type":"integer","description":"Determines the size in bytes of the generated key.","default":20},"max_validation_attempts":{"type":"integer","description":"Max number of allowed validation attempts."},"method_id":{"type":"string","description":"The unique identifier for this MFA method."},"period":{"type":"integer","description":"The length of time used to generate a counter for the TOTP token calculation.","format":"seconds","default":30},"qr_size":{"type":"integer","description":"The pixel size of the generated square QR code.","default":200},"skew":{"type":"integer","description":"The number of delay periods that are allowed when validating a TOTP token. This value can either be 0 or 1.","default":1}}},"IdentityOidcAssignmentRequest":{"type":"object","properties":{"entity_ids":{"type":"array","description":"Comma separated string or array of identity entity IDs","items":{"type":"string"}},"group_ids":{"type":"array","description":"Comma separated string or array of identity group IDs","items":{"type":"string"}}}},"IdentityOidcClientRequest":{"type":"object","properties":{"access_token_ttl":{"type":"integer","description":"The time-to-live for access tokens obtained by the client.","format":"seconds","default":"24h"},"assignments":{"type":"array","description":"Comma separated string or array of assignment resources.","items":{"type":"string"}},"client_type":{"type":"string","description":"The client type based on its ability to maintain confidentiality of credentials. The following client types are supported: 'confidential', 'public'. Defaults to 'confidential'.","default":"confidential"},"id_token_ttl":{"type":"integer","description":"The time-to-live for ID tokens obtained by the client.","format":"seconds","default":"24h"},"key":{"type":"string","description":"A reference to a named key resource. Cannot be modified after creation. Defaults to the 'default' key.","default":"default"},"redirect_uris":{"type":"array","description":"Comma separated string or array of redirect URIs used by the client. One of these values must exactly match the redirect_uri parameter value used in each authentication request.","items":{"type":"string"}}}},"IdentityOidcConfigRequest":{"type":"object","properties":{"issuer":{"type":"string","description":"Issuer URL to be used in the iss claim of the token. If not set, Vault's app_addr will be used."}}},"IdentityOidcIntrospectRequest":{"type":"object","properties":{"client_id":{"type":"string","description":"Optional client_id to verify"},"token":{"type":"string","description":"Token to verify"}}},"IdentityOidcKeyRequest":{"type":"object","properties":{"algorithm":{"type":"string","description":"Signing algorithm to use. This will default to RS256.","default":"RS256"},"allowed_client_ids":{"type":"array","description":"Comma separated string or array of role client ids allowed to use this key for signing. If empty no roles are allowed. If \"*\" all roles are allowed.","items":{"type":"string"}},"rotation_period":{"type":"integer","description":"How often to generate a new keypair.","format":"seconds","default":"24h"},"verification_ttl":{"type":"integer","description":"Controls how long the public portion of a key will be available for verification after being rotated.","format":"seconds","default":"24h"}}},"IdentityOidcKeyRotateRequest":{"type":"object","properties":{"verification_ttl":{"type":"integer","description":"Controls how long the public portion of a key will be available for verification after being rotated. Setting verification_ttl here will override the verification_ttl set on the key.","format":"seconds"}}},"IdentityOidcProviderAuthorizeRequest":{"type":"object","properties":{"client_id":{"type":"string","description":"The ID of the requesting client."},"code_challenge":{"type":"string","description":"The code challenge derived from the code verifier."},"code_challenge_method":{"type":"string","description":"The method that was used to derive the code challenge. The following methods are supported: 'S256', 'plain'. Defaults to 'plain'.","default":"plain"},"max_age":{"type":"integer","description":"The allowable elapsed time in seconds since the last time the end-user was actively authenticated."},"nonce":{"type":"string","description":"The value that will be returned in the ID token nonce claim after a token exchange."},"redirect_uri":{"type":"string","description":"The redirection URI to which the response will be sent."},"response_type":{"type":"string","description":"The OIDC authentication flow to be used. The following response types are supported: 'code'"},"scope":{"type":"string","description":"A space-delimited, case-sensitive list of scopes to be requested. The 'openid' scope is required."},"state":{"type":"string","description":"The value used to maintain state between the authentication request and client."}},"required":["redirect_uri","client_id","response_type","scope"]},"IdentityOidcProviderRequest":{"type":"object","properties":{"allowed_client_ids":{"type":"array","description":"The client IDs that are permitted to use the provider","items":{"type":"string"}},"issuer":{"type":"string","description":"Specifies what will be used for the iss claim of ID tokens."},"scopes_supported":{"type":"array","description":"The scopes supported for requesting on the provider","items":{"type":"string"}}}},"IdentityOidcProviderTokenRequest":{"type":"object","properties":{"client_id":{"type":"string","description":"The ID of the requesting client."},"client_secret":{"type":"string","description":"The secret of the requesting client."},"code":{"type":"string","description":"The authorization code received from the provider's authorization endpoint."},"code_verifier":{"type":"string","description":"The code verifier associated with the authorization code."},"grant_type":{"type":"string","description":"The authorization grant type. The following grant types are supported: 'authorization_code'."},"redirect_uri":{"type":"string","description":"The callback location where the authentication response was sent."}},"required":["redirect_uri","code","grant_type"]},"IdentityOidcRoleRequest":{"type":"object","properties":{"client_id":{"type":"string","description":"Optional client_id"},"key":{"type":"string","description":"The OIDC key to use for generating tokens. The specified key must already exist."},"template":{"type":"string","description":"The template string to use for generating tokens. This may be in string-ified JSON or base64 format."},"ttl":{"type":"integer","description":"TTL of the tokens generated against the role.","format":"seconds","default":"24h"}},"required":["key"]},"IdentityOidcScopeRequest":{"type":"object","properties":{"description":{"type":"string","description":"The description of the scope"},"template":{"type":"string","description":"The template string to use for the scope. This may be in string-ified JSON or base64 format."}}},"IdentityPersonaIdRequest":{"type":"object","properties":{"entity_id":{"type":"string","description":"Entity ID to which this persona should be tied to"},"metadata":{"type":"object","description":"Metadata to be associated with the persona. In CLI, this parameter can be repeated multiple times, and it all gets merged together. For example: vault \u003ccommand\u003e \u003cpath\u003e metadata=key1=value1 metadata=key2=value2","format":"kvpairs"},"mount_accessor":{"type":"string","description":"Mount accessor to which this persona belongs to"},"name":{"type":"string","description":"Name of the persona"}}},"IdentityPersonaRequest":{"type":"object","properties":{"entity_id":{"type":"string","description":"Entity ID to which this persona belongs to"},"id":{"type":"string","description":"ID of the persona"},"metadata":{"type":"object","description":"Metadata to be associated with the persona. In CLI, this parameter can be repeated multiple times, and it all gets merged together. For example: vault \u003ccommand\u003e \u003cpath\u003e metadata=key1=value1 metadata=key2=value2","format":"kvpairs"},"mount_accessor":{"type":"string","description":"Mount accessor to which this persona belongs to"},"name":{"type":"string","description":"Name of the persona"}}},"KvConfigRequest":{"type":"object","properties":{"cas_required":{"type":"boolean","description":"If true, the backend will require the cas parameter to be set for each write"},"delete_version_after":{"type":"integer","description":"If set, the length of time before a version is deleted. A negative duration disables the use of delete_version_after on all keys. A zero duration clears the current setting. Accepts a Go duration format string.","format":"seconds"},"max_versions":{"type":"integer","description":"The number of versions to keep for each key. Defaults to 10"}}},"KvDataRequest":{"type":"object","properties":{"data":{"type":"object","description":"The contents of the data map will be stored and returned on read.","format":"map"},"options":{"type":"object","description":"Options for writing a KV entry. Set the \"cas\" value to use a Check-And-Set operation. If not set the write will be allowed. If set to 0 a write will only be allowed if the key doesn’t exist. If the index is non-zero the write will only be allowed if the key’s current version matches the version specified in the cas parameter.","format":"map"},"version":{"type":"integer","description":"If provided during a read, the value at the version number will be returned"}}},"KvDeleteRequest":{"type":"object","properties":{"versions":{"type":"array","description":"The versions to be archived. The versioned data will not be deleted, but it will no longer be returned in normal get requests.","items":{"type":"integer"}}}},"KvDestroyRequest":{"type":"object","properties":{"versions":{"type":"array","description":"The versions to destroy. Their data will be permanently deleted.","items":{"type":"integer"}}}},"KvMetadataRequest":{"type":"object","properties":{"cas_required":{"type":"boolean","description":"If true the key will require the cas parameter to be set on all write requests. If false, the backend’s configuration will be used."},"custom_metadata":{"type":"object","description":"User-provided key-value pairs that are used to describe arbitrary and version-agnostic information about a secret.","format":"map"},"delete_version_after":{"type":"integer","description":"The length of time before a version is deleted. If not set, the backend's configured delete_version_after is used. Cannot be greater than the backend's delete_version_after. A zero duration clears the current setting. A negative duration will cause an error.","format":"seconds"},"max_versions":{"type":"integer","description":"The number of versions to keep. If not set, the backend’s configured max version is used."}}},"KvUndeleteRequest":{"type":"object","properties":{"versions":{"type":"array","description":"The versions to unarchive. The versions will be restored and their data will be returned on normal get requests.","items":{"type":"integer"}}}},"SystemAuditHashRequest":{"type":"object","properties":{"input":{"type":"string"}}},"SystemAuditRequest":{"type":"object","properties":{"description":{"type":"string","description":"User-friendly description for this audit backend."},"local":{"type":"boolean","description":"Mark the mount as a local mount, which is not replicated and is unaffected by replication.","default":false},"options":{"type":"object","description":"Configuration options for the audit backend.","format":"kvpairs"},"type":{"type":"string","description":"The type of the backend. Example: \"mysql\""}}},"SystemAuthRequest":{"type":"object","properties":{"config":{"type":"object","description":"Configuration for this mount, such as plugin_name.","format":"map"},"description":{"type":"string","description":"User-friendly description for this credential backend."},"external_entropy_access":{"type":"boolean","description":"Whether to give the mount access to Vault's external entropy.","default":false},"local":{"type":"boolean","description":"Mark the mount as a local mount, which is not replicated and is unaffected by replication.","default":false},"options":{"type":"object","description":"The options to pass into the backend. Should be a json object with string keys and values.","format":"kvpairs"},"plugin_name":{"type":"string","description":"Name of the auth plugin to use based from the name in the plugin catalog."},"plugin_version":{"type":"string","description":"The semantic version of the plugin to use."},"seal_wrap":{"type":"boolean","description":"Whether to turn on seal wrapping for the mount.","default":false},"type":{"type":"string","description":"The type of the backend. Example: \"userpass\""}}},"SystemAuthTuneRequest":{"type":"object","properties":{"allowed_response_headers":{"type":"array","description":"A list of headers to whitelist and allow a plugin to set on responses.","items":{"type":"string"}},"audit_non_hmac_request_keys":{"type":"array","description":"The list of keys in the request data object that will not be HMAC'ed by audit devices.","items":{"type":"string"}},"audit_non_hmac_response_keys":{"type":"array","description":"The list of keys in the response data object that will not be HMAC'ed by audit devices.","items":{"type":"string"}},"default_lease_ttl":{"type":"string","description":"The default lease TTL for this mount."},"description":{"type":"string","description":"User-friendly description for this credential backend."},"listing_visibility":{"type":"string","description":"Determines the visibility of the mount in the UI-specific listing endpoint. Accepted value are 'unauth' and 'hidden', with the empty default ('') behaving like 'hidden'."},"max_lease_ttl":{"type":"string","description":"The max lease TTL for this mount."},"options":{"type":"object","description":"The options to pass into the backend. Should be a json object with string keys and values.","format":"kvpairs"},"passthrough_request_headers":{"type":"array","description":"A list of headers to whitelist and pass from the request to the plugin.","items":{"type":"string"}},"plugin_version":{"type":"string","description":"The semantic version of the plugin to use."},"token_type":{"type":"string","description":"The type of token to issue (service or batch)."}}},"SystemCapabilitiesAccessorRequest":{"type":"object","properties":{"accessor":{"type":"string","description":"Accessor of the token for which capabilities are being queried."},"path":{"type":"array","description":"Use 'paths' instead.","items":{"type":"string"},"deprecated":true},"paths":{"type":"array","description":"Paths on which capabilities are being queried.","items":{"type":"string"}}}},"SystemCapabilitiesRequest":{"type":"object","properties":{"path":{"type":"array","description":"Use 'paths' instead.","items":{"type":"string"},"deprecated":true},"paths":{"type":"array","description":"Paths on which capabilities are being queried.","items":{"type":"string"}},"token":{"type":"string","description":"Token for which capabilities are being queried."}}},"SystemCapabilitiesSelfRequest":{"type":"object","properties":{"path":{"type":"array","description":"Use 'paths' instead.","items":{"type":"string"},"deprecated":true},"paths":{"type":"array","description":"Paths on which capabilities are being queried.","items":{"type":"string"}},"token":{"type":"string","description":"Token for which capabilities are being queried."}}},"SystemConfigAuditingRequestHeadersRequest":{"type":"object","properties":{"hmac":{"type":"boolean"}}},"SystemConfigCorsRequest":{"type":"object","properties":{"allowed_headers":{"type":"array","description":"A comma-separated string or array of strings indicating headers that are allowed on cross-origin requests.","items":{"type":"string"}},"allowed_origins":{"type":"array","description":"A comma-separated string or array of strings indicating origins that may make cross-origin requests.","items":{"type":"string"}},"enable":{"type":"boolean","description":"Enables or disables CORS headers on requests."}}},"SystemConfigUiHeadersRequest":{"type":"object","properties":{"multivalue":{"type":"boolean","description":"Returns multiple values if true"},"values":{"type":"array","description":"The values to set the header.","items":{"type":"string"}}}},"SystemGenerateRootAttemptRequest":{"type":"object","properties":{"pgp_key":{"type":"string","description":"Specifies a base64-encoded PGP public key."}}},"SystemGenerateRootRequest":{"type":"object","properties":{"pgp_key":{"type":"string","description":"Specifies a base64-encoded PGP public key."}}},"SystemGenerateRootUpdateRequest":{"type":"object","properties":{"key":{"type":"string","description":"Specifies a single unseal key share."},"nonce":{"type":"string","description":"Specifies the nonce of the attempt."}}},"SystemInitRequest":{"type":"object","properties":{"pgp_keys":{"type":"array","description":"Specifies an array of PGP public keys used to encrypt the output unseal keys. Ordering is preserved. The keys must be base64-encoded from their original binary representation. The size of this array must be the same as `secret_shares`.","items":{"type":"string"}},"recovery_pgp_keys":{"type":"array","description":"Specifies an array of PGP public keys used to encrypt the output recovery keys. Ordering is preserved. The keys must be base64-encoded from their original binary representation. The size of this array must be the same as `recovery_shares`.","items":{"type":"string"}},"recovery_shares":{"type":"integer","description":"Specifies the number of shares to split the recovery key into."},"recovery_threshold":{"type":"integer","description":"Specifies the number of shares required to reconstruct the recovery key. This must be less than or equal to `recovery_shares`."},"root_token_pgp_key":{"type":"string","description":"Specifies a PGP public key used to encrypt the initial root token. The key must be base64-encoded from its original binary representation."},"secret_shares":{"type":"integer","description":"Specifies the number of shares to split the unseal key into."},"secret_threshold":{"type":"integer","description":"Specifies the number of shares required to reconstruct the unseal key. This must be less than or equal secret_shares. If using Vault HSM with auto-unsealing, this value must be the same as `secret_shares`."},"stored_shares":{"type":"integer","description":"Specifies the number of shares that should be encrypted by the HSM and stored for auto-unsealing. Currently must be the same as `secret_shares`."}}},"SystemInternalCountersConfigRequest":{"type":"object","properties":{"default_report_months":{"type":"integer","description":"Number of months to report if no start date specified.","default":12},"enabled":{"type":"string","description":"Enable or disable collection of client count: enable, disable, or default.","default":"default"},"retention_months":{"type":"integer","description":"Number of months of client data to retain. Setting to 0 will clear all existing data.","default":24}}},"SystemInternalSpecsOpenapiRequest":{"type":"object","properties":{"context":{"type":"string","description":"Context string appended to every operationId"}}},"SystemLeasesLookupRequest":{"type":"object","properties":{"lease_id":{"type":"string","description":"The lease identifier to renew. This is included with a lease."}}},"SystemLeasesRenewLeaseRequest":{"type":"object","properties":{"increment":{"type":"integer","description":"The desired increment in seconds to the lease","format":"seconds"},"lease_id":{"type":"string","description":"The lease identifier to renew. This is included with a lease."}}},"SystemLeasesRenewRequest":{"type":"object","properties":{"increment":{"type":"integer","description":"The desired increment in seconds to the lease","format":"seconds"},"lease_id":{"type":"string","description":"The lease identifier to renew. This is included with a lease."},"url_lease_id":{"type":"string","description":"The lease identifier to renew. This is included with a lease."}}},"SystemLeasesRevokeLeaseRequest":{"type":"object","properties":{"lease_id":{"type":"string","description":"The lease identifier to renew. This is included with a lease."},"sync":{"type":"boolean","description":"Whether or not to perform the revocation synchronously","default":true}}},"SystemLeasesRevokePrefixRequest":{"type":"object","properties":{"sync":{"type":"boolean","description":"Whether or not to perform the revocation synchronously","default":true}}},"SystemLeasesRevokeRequest":{"type":"object","properties":{"lease_id":{"type":"string","description":"The lease identifier to renew. This is included with a lease."},"sync":{"type":"boolean","description":"Whether or not to perform the revocation synchronously","default":true},"url_lease_id":{"type":"string","description":"The lease identifier to renew. This is included with a lease."}}},"SystemLoggersRequest":{"type":"object","properties":{"level":{"type":"string","description":"Log verbosity level. Supported values (in order of detail) are \"trace\", \"debug\", \"info\", \"warn\", and \"error\"."}}},"SystemMfaValidateRequest":{"type":"object","properties":{"mfa_payload":{"type":"object","description":"A map from MFA method ID to a slice of passcodes or an empty slice if the method does not use passcodes","format":"map"},"mfa_request_id":{"type":"string","description":"ID for this MFA request"}},"required":["mfa_request_id","mfa_payload"]},"SystemMountsRequest":{"type":"object","properties":{"config":{"type":"object","description":"Configuration for this mount, such as default_lease_ttl and max_lease_ttl.","format":"map"},"description":{"type":"string","description":"User-friendly description for this mount."},"external_entropy_access":{"type":"boolean","description":"Whether to give the mount access to Vault's external entropy.","default":false},"local":{"type":"boolean","description":"Mark the mount as a local mount, which is not replicated and is unaffected by replication.","default":false},"options":{"type":"object","description":"The options to pass into the backend. Should be a json object with string keys and values.","format":"kvpairs"},"plugin_name":{"type":"string","description":"Name of the plugin to mount based from the name registered in the plugin catalog."},"plugin_version":{"type":"string","description":"The semantic version of the plugin to use."},"seal_wrap":{"type":"boolean","description":"Whether to turn on seal wrapping for the mount.","default":false},"type":{"type":"string","description":"The type of the backend. Example: \"passthrough\""}}},"SystemMountsTuneRequest":{"type":"object","properties":{"allowed_managed_keys":{"type":"array","items":{"type":"string"}},"allowed_response_headers":{"type":"array","description":"A list of headers to whitelist and allow a plugin to set on responses.","items":{"type":"string"}},"audit_non_hmac_request_keys":{"type":"array","description":"The list of keys in the request data object that will not be HMAC'ed by audit devices.","items":{"type":"string"}},"audit_non_hmac_response_keys":{"type":"array","description":"The list of keys in the response data object that will not be HMAC'ed by audit devices.","items":{"type":"string"}},"default_lease_ttl":{"type":"string","description":"The default lease TTL for this mount."},"description":{"type":"string","description":"User-friendly description for this credential backend."},"listing_visibility":{"type":"string","description":"Determines the visibility of the mount in the UI-specific listing endpoint. Accepted value are 'unauth' and 'hidden', with the empty default ('') behaving like 'hidden'."},"max_lease_ttl":{"type":"string","description":"The max lease TTL for this mount."},"options":{"type":"object","description":"The options to pass into the backend. Should be a json object with string keys and values.","format":"kvpairs"},"passthrough_request_headers":{"type":"array","description":"A list of headers to whitelist and pass from the request to the plugin.","items":{"type":"string"}},"plugin_version":{"type":"string","description":"The semantic version of the plugin to use."},"token_type":{"type":"string","description":"The type of token to issue (service or batch)."}}},"SystemPluginsCatalogRequest":{"type":"object","properties":{"args":{"type":"array","description":"The args passed to plugin command.","items":{"type":"string"}},"command":{"type":"string","description":"The command used to start the plugin. The executable defined in this command must exist in vault's plugin directory."},"env":{"type":"array","description":"The environment variables passed to plugin command. Each entry is of the form \"key=value\".","items":{"type":"string"}},"sha256":{"type":"string","description":"The SHA256 sum of the executable used in the command field. This should be HEX encoded."},"type":{"type":"string","description":"The type of the plugin, may be auth, secret, or database"},"version":{"type":"string","description":"The semantic version of the plugin to use."}}},"SystemPluginsReloadBackendRequest":{"type":"object","properties":{"mounts":{"type":"array","description":"The mount paths of the plugin backends to reload.","items":{"type":"string"}},"plugin":{"type":"string","description":"The name of the plugin to reload, as registered in the plugin catalog."},"scope":{"type":"string"}}},"SystemPoliciesAclRequest":{"type":"object","properties":{"policy":{"type":"string","description":"The rules of the policy."}}},"SystemPoliciesPasswordRequest":{"type":"object","properties":{"policy":{"type":"string","description":"The password policy"}}},"SystemPolicyRequest":{"type":"object","properties":{"policy":{"type":"string","description":"The rules of the policy."},"rules":{"type":"string","description":"The rules of the policy.","deprecated":true}}},"SystemQuotasConfigRequest":{"type":"object","properties":{"enable_rate_limit_audit_logging":{"type":"boolean","description":"If set, starts audit logging of requests that get rejected due to rate limit quota rule violations."},"enable_rate_limit_response_headers":{"type":"boolean","description":"If set, additional rate limit quota HTTP headers will be added to responses."},"rate_limit_exempt_paths":{"type":"array","description":"Specifies the list of exempt paths from all rate limit quotas. If empty no paths will be exempt.","items":{"type":"string"}}}},"SystemQuotasRateLimitRequest":{"type":"object","properties":{"block_interval":{"type":"integer","description":"If set, when a client reaches a rate limit threshold, the client will be prohibited from any further requests until after the 'block_interval' has elapsed.","format":"seconds"},"interval":{"type":"integer","description":"The duration to enforce rate limiting for (default '1s').","format":"seconds"},"path":{"type":"string","description":"Path of the mount or namespace to apply the quota. A blank path configures a global quota. For example namespace1/ adds a quota to a full namespace, namespace1/auth/userpass adds a quota to userpass in namespace1."},"rate":{"type":"number","description":"The maximum number of requests in a given interval to be allowed by the quota rule. The 'rate' must be positive.","format":"float"},"role":{"type":"string","description":"Login role to apply this quota to. Note that when set, path must be configured to a valid auth method with a concept of roles."},"type":{"type":"string","description":"Type of the quota rule."}}},"SystemRawRequest":{"type":"object","properties":{"compressed":{"type":"boolean"},"compression_type":{"type":"string"},"encoding":{"type":"string"},"value":{"type":"string"}}},"SystemRekeyInitRequest":{"type":"object","properties":{"backup":{"type":"boolean","description":"Specifies if using PGP-encrypted keys, whether Vault should also store a plaintext backup of the PGP-encrypted keys."},"pgp_keys":{"type":"array","description":"Specifies an array of PGP public keys used to encrypt the output unseal keys. Ordering is preserved. The keys must be base64-encoded from their original binary representation. The size of this array must be the same as secret_shares.","items":{"type":"string"}},"require_verification":{"type":"boolean","description":"Turns on verification functionality"},"secret_shares":{"type":"integer","description":"Specifies the number of shares to split the unseal key into."},"secret_threshold":{"type":"integer","description":"Specifies the number of shares required to reconstruct the unseal key. This must be less than or equal secret_shares. If using Vault HSM with auto-unsealing, this value must be the same as secret_shares."}}},"SystemRekeyUpdateRequest":{"type":"object","properties":{"key":{"type":"string","description":"Specifies a single unseal key share."},"nonce":{"type":"string","description":"Specifies the nonce of the rekey attempt."}}},"SystemRekeyVerifyRequest":{"type":"object","properties":{"key":{"type":"string","description":"Specifies a single unseal share key from the new set of shares."},"nonce":{"type":"string","description":"Specifies the nonce of the rekey verification operation."}}},"SystemRemountRequest":{"type":"object","properties":{"from":{"type":"string","description":"The previous mount point."},"to":{"type":"string","description":"The new mount point."}}},"SystemRenewLeaseRequest":{"type":"object","properties":{"increment":{"type":"integer","description":"The desired increment in seconds to the lease","format":"seconds"},"lease_id":{"type":"string","description":"The lease identifier to renew. This is included with a lease."}}},"SystemRenewRequest":{"type":"object","properties":{"increment":{"type":"integer","description":"The desired increment in seconds to the lease","format":"seconds"},"lease_id":{"type":"string","description":"The lease identifier to renew. This is included with a lease."},"url_lease_id":{"type":"string","description":"The lease identifier to renew. This is included with a lease."}}},"SystemRevokeLeaseRequest":{"type":"object","properties":{"lease_id":{"type":"string","description":"The lease identifier to renew. This is included with a lease."},"sync":{"type":"boolean","description":"Whether or not to perform the revocation synchronously","default":true}}},"SystemRevokePrefixRequest":{"type":"object","properties":{"sync":{"type":"boolean","description":"Whether or not to perform the revocation synchronously","default":true}}},"SystemRevokeRequest":{"type":"object","properties":{"lease_id":{"type":"string","description":"The lease identifier to renew. This is included with a lease."},"sync":{"type":"boolean","description":"Whether or not to perform the revocation synchronously","default":true},"url_lease_id":{"type":"string","description":"The lease identifier to renew. This is included with a lease."}}},"SystemRotateConfigRequest":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Whether automatic rotation is enabled."},"interval":{"type":"integer","description":"How long after installation of an active key term that the key will be automatically rotated.","format":"seconds"},"max_operations":{"type":"integer","description":"The number of encryption operations performed before the barrier key is automatically rotated.","format":"int64"}}},"SystemToolsHashRequest":{"type":"object","properties":{"algorithm":{"type":"string","description":"Algorithm to use (POST body parameter). Valid values are: * sha2-224 * sha2-256 * sha2-384 * sha2-512 Defaults to \"sha2-256\".","default":"sha2-256"},"format":{"type":"string","description":"Encoding format to use. Can be \"hex\" or \"base64\". Defaults to \"hex\".","default":"hex"},"input":{"type":"string","description":"The base64-encoded input data"},"urlalgorithm":{"type":"string","description":"Algorithm to use (POST URL parameter)"}}},"SystemToolsRandomRequest":{"type":"object","properties":{"bytes":{"type":"integer","description":"The number of bytes to generate (POST body parameter). Defaults to 32 (256 bits).","default":32},"format":{"type":"string","description":"Encoding format to use. Can be \"hex\" or \"base64\". Defaults to \"base64\".","default":"base64"},"source":{"type":"string","description":"Which system to source random data from, ether \"platform\", \"seal\", or \"all\".","default":"platform"},"urlbytes":{"type":"string","description":"The number of bytes to generate (POST URL parameter)"}}},"SystemUnsealRequest":{"type":"object","properties":{"key":{"type":"string","description":"Specifies a single unseal key share. This is required unless reset is true."},"reset":{"type":"boolean","description":"Specifies if previously-provided unseal keys are discarded and the unseal process is reset."}}},"SystemWrappingLookupRequest":{"type":"object","properties":{"token":{"type":"string"}}},"SystemWrappingRewrapRequest":{"type":"object","properties":{"token":{"type":"string"}}},"SystemWrappingUnwrapRequest":{"type":"object","properties":{"token":{"type":"string"}}},"TokenCreateOrphanRequest":{"type":"object","properties":{"display_name":{"type":"string","description":"Name to associate with this token"},"entity_alias":{"type":"string","description":"Name of the entity alias to associate with this token"},"explicit_max_ttl":{"type":"string","description":"Explicit Max TTL of this token"},"id":{"type":"string","description":"Value for the token"},"metadata":{"type":"object","description":"Arbitrary key=value metadata to associate with the token","format":"map"},"no_default_policy":{"type":"boolean","description":"Do not include default policy for this token"},"no_parent":{"type":"boolean","description":"Create the token with no parent"},"num_uses":{"type":"integer","description":"Max number of uses for this token"},"period":{"type":"string","description":"Renew period"},"policies":{"type":"array","description":"List of policies for the token","items":{"type":"string"}},"renewable":{"type":"boolean","description":"Allow token to be renewed past its initial TTL up to system/mount maximum TTL"},"role_name":{"type":"string","description":"Name of the role"},"ttl":{"type":"string","description":"Time to live for this token"},"type":{"type":"string","description":"Token type"}}},"TokenCreateRequest":{"type":"object","properties":{"display_name":{"type":"string","description":"Name to associate with this token"},"entity_alias":{"type":"string","description":"Name of the entity alias to associate with this token"},"explicit_max_ttl":{"type":"string","description":"Explicit Max TTL of this token"},"id":{"type":"string","description":"Value for the token"},"metadata":{"type":"object","description":"Arbitrary key=value metadata to associate with the token","format":"map"},"no_default_policy":{"type":"boolean","description":"Do not include default policy for this token"},"no_parent":{"type":"boolean","description":"Create the token with no parent"},"num_uses":{"type":"integer","description":"Max number of uses for this token"},"period":{"type":"string","description":"Renew period"},"policies":{"type":"array","description":"List of policies for the token","items":{"type":"string"}},"renewable":{"type":"boolean","description":"Allow token to be renewed past its initial TTL up to system/mount maximum TTL"},"ttl":{"type":"string","description":"Time to live for this token"},"type":{"type":"string","description":"Token type"}}},"TokenLookupAccessorRequest":{"type":"object","properties":{"accessor":{"type":"string","description":"Accessor of the token to look up (request body)"}}},"TokenLookupRequest":{"type":"object","properties":{"token":{"type":"string","description":"Token to lookup (POST request body)"}}},"TokenLookupSelfRequest":{"type":"object","properties":{"token":{"type":"string","description":"Token to look up (unused, does not need to be set)"}}},"TokenRenewAccessorRequest":{"type":"object","properties":{"accessor":{"type":"string","description":"Accessor of the token to renew (request body)"},"increment":{"type":"integer","description":"The desired increment in seconds to the token expiration","format":"seconds","default":0}}},"TokenRenewRequest":{"type":"object","properties":{"increment":{"type":"integer","description":"The desired increment in seconds to the token expiration","format":"seconds","default":0},"token":{"type":"string","description":"Token to renew (request body)"}}},"TokenRenewSelfRequest":{"type":"object","properties":{"increment":{"type":"integer","description":"The desired increment in seconds to the token expiration","format":"seconds","default":0},"token":{"type":"string","description":"Token to renew (unused, does not need to be set)"}}},"TokenRevokeAccessorRequest":{"type":"object","properties":{"accessor":{"type":"string","description":"Accessor of the token (request body)"}}},"TokenRevokeOrphanRequest":{"type":"object","properties":{"token":{"type":"string","description":"Token to revoke (request body)"}}},"TokenRevokeRequest":{"type":"object","properties":{"token":{"type":"string","description":"Token to revoke (request body)"}}},"TokenRolesRequest":{"type":"object","properties":{"allowed_entity_aliases":{"type":"array","description":"String or JSON list of allowed entity aliases. If set, specifies the entity aliases which are allowed to be used during token generation. This field supports globbing.","items":{"type":"string"}},"allowed_policies":{"type":"array","description":"If set, tokens can be created with any subset of the policies in this list, rather than the normal semantics of tokens being a subset of the calling token's policies. The parameter is a comma-delimited string of policy names.","items":{"type":"string"}},"allowed_policies_glob":{"type":"array","description":"If set, tokens can be created with any subset of glob matched policies in this list, rather than the normal semantics of tokens being a subset of the calling token's policies. The parameter is a comma-delimited string of policy name globs.","items":{"type":"string"}},"bound_cidrs":{"type":"array","description":"Use 'token_bound_cidrs' instead.","items":{"type":"string"},"deprecated":true},"disallowed_policies":{"type":"array","description":"If set, successful token creation via this role will require that no policies in the given list are requested. The parameter is a comma-delimited string of policy names.","items":{"type":"string"}},"disallowed_policies_glob":{"type":"array","description":"If set, successful token creation via this role will require that no requested policies glob match any of policies in this list. The parameter is a comma-delimited string of policy name globs.","items":{"type":"string"}},"explicit_max_ttl":{"type":"integer","description":"Use 'token_explicit_max_ttl' instead.","format":"seconds","deprecated":true},"orphan":{"type":"boolean","description":"If true, tokens created via this role will be orphan tokens (have no parent)"},"path_suffix":{"type":"string","description":"If set, tokens created via this role will contain the given suffix as a part of their path. This can be used to assist use of the 'revoke-prefix' endpoint later on. The given suffix must match the regular expression.\\w[\\w-.]+\\w"},"period":{"type":"integer","description":"Use 'token_period' instead.","format":"seconds","deprecated":true},"renewable":{"type":"boolean","description":"Tokens created via this role will be renewable or not according to this value. Defaults to \"true\".","default":true},"token_bound_cidrs":{"type":"array","description":"Comma separated string or JSON list of CIDR blocks. If set, specifies the blocks of IP addresses which are allowed to use the generated token.","items":{"type":"string"},"x-vault-displayAttrs":{"name":"Generated Token's Bound CIDRs","group":"Tokens"}},"token_explicit_max_ttl":{"type":"integer","description":"If set, tokens created via this role carry an explicit maximum TTL. During renewal, the current maximum TTL values of the role and the mount are not checked for changes, and any updates to these values will have no effect on the token being renewed.","format":"seconds","x-vault-displayAttrs":{"name":"Generated Token's Explicit Maximum TTL","group":"Tokens"}},"token_no_default_policy":{"type":"boolean","description":"If true, the 'default' policy will not automatically be added to generated tokens","x-vault-displayAttrs":{"name":"Do Not Attach 'default' Policy To Generated Tokens","group":"Tokens"}},"token_num_uses":{"type":"integer","description":"The maximum number of times a token may be used, a value of zero means unlimited","x-vault-displayAttrs":{"name":"Maximum Uses of Generated Tokens","group":"Tokens"}},"token_period":{"type":"integer","description":"If set, tokens created via this role will have no max lifetime; instead, their renewal period will be fixed to this value. This takes an integer number of seconds, or a string duration (e.g. \"24h\").","format":"seconds","x-vault-displayAttrs":{"name":"Generated Token's Period","group":"Tokens"}},"token_type":{"type":"string","description":"The type of token to generate, service or batch","default":"default-service","x-vault-displayAttrs":{"name":"Generated Token's Type","group":"Tokens"}}}}}}} \ No newline at end of file diff --git a/secret-provider/src/it/scala/io/lenses/connect/secrets/integration/SecretProviderRestResponse.scala b/secret-provider/src/it/scala/io/lenses/connect/secrets/integration/SecretProviderRestResponse.scala new file mode 100644 index 0000000..4a2fe3d --- /dev/null +++ b/secret-provider/src/it/scala/io/lenses/connect/secrets/integration/SecretProviderRestResponse.scala @@ -0,0 +1,33 @@ +package io.lenses.connect.secrets.integration + +import com.bettercloud.vault.rest.RestResponse +import org.json4s.DefaultFormats +import org.json4s._ +import org.json4s.native.JsonMethods._ + +case class DataResponse( + data: Map[String, Any], + metadata: Map[String, Any], +) +case class SecretProviderRestResponse( + requestId: String, + leaseId: String, + renewable: Boolean, + leaseDuration: BigInt, + data: DataResponse, + wrapInfo: Any, // todo what is this? + warnings: Any, // todo + auth: Any, // todo +) + +case object SecretProviderRestResponse { + + def fromRestResponse(rr: RestResponse) = + fromJson(new String(rr.getBody)) + + def fromJson(json: String) = { + implicit val formats: DefaultFormats.type = DefaultFormats + + parse(json).camelizeKeys.extract[SecretProviderRestResponse] + } +} diff --git a/secret-provider/src/it/scala/io/lenses/connect/secrets/integration/SecretProviderRestResponseTest.scala b/secret-provider/src/it/scala/io/lenses/connect/secrets/integration/SecretProviderRestResponseTest.scala new file mode 100644 index 0000000..c7d78fe --- /dev/null +++ b/secret-provider/src/it/scala/io/lenses/connect/secrets/integration/SecretProviderRestResponseTest.scala @@ -0,0 +1,28 @@ +package io.lenses.connect.secrets.integration + +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class SecretProviderRestResponseTest extends AnyFunSuite with Matchers { + + private val sampleJson = + """{"request_id":"41398853-3a2f-ba6b-7c37-23b201941071","lease_id":"","renewable":false,"lease_duration":20,"data": {"data":{"myVaultSecretKey":"myVaultSecretValue"}, "metadata":{"created_time":"2023-02-28T10:49:56.854615Z","custom_metadata":null,"deletion_time":"","destroyed":false,"version":1}},"wrap_info":null,"warnings":null,"auth":null}""" + + test("Should unmarshal sample json") { + val sprr = SecretProviderRestResponse.fromJson(sampleJson) + + sprr.requestId should be("41398853-3a2f-ba6b-7c37-23b201941071") + sprr.leaseDuration should be(20) + sprr.data.data should be(Map[String, Any]("myVaultSecretKey" -> "myVaultSecretValue")) + sprr.data.metadata.toSet should be( + Set( + "createdTime" -> "2023-02-28T10:49:56.854615Z", + "customMetadata" -> null, + "deletionTime" -> "", + "destroyed" -> false, + "version" -> Integer.valueOf(1), + ), + ) + } + +} diff --git a/secret-provider/src/it/scala/io/lenses/connect/secrets/integration/VaultSecretProviderIT.scala b/secret-provider/src/it/scala/io/lenses/connect/secrets/integration/VaultSecretProviderIT.scala new file mode 100644 index 0000000..6576be7 --- /dev/null +++ b/secret-provider/src/it/scala/io/lenses/connect/secrets/integration/VaultSecretProviderIT.scala @@ -0,0 +1,163 @@ +package io.lenses.connect.secrets.integration + +import cats.effect.FiberIO +import cats.effect.IO +import cats.effect.Ref +import cats.effect.Resource +import cats.effect.unsafe.implicits.global +import cats.implicits.catsSyntaxOptionId +import io.lenses.connect.secrets.testcontainers.LeaseInfo +import io.lenses.connect.secrets.testcontainers.VaultContainer +import io.lenses.connect.secrets.testcontainers.VaultContainer.vaultSecretKey +import io.lenses.connect.secrets.testcontainers.VaultContainer.vaultSecretPath +import io.lenses.connect.secrets.testcontainers.connect.ConfigProvider +import io.lenses.connect.secrets.testcontainers.connect.ConfigProviders +import io.lenses.connect.secrets.testcontainers.connect.ConnectorConfiguration +import io.lenses.connect.secrets.testcontainers.connect.KafkaConnectClient +import io.lenses.connect.secrets.testcontainers.connect.StringCnfVal +import io.lenses.connect.secrets.testcontainers.scalatest.SecretProviderContainerPerSuite +import org.apache.kafka.clients.producer.KafkaProducer +import org.apache.kafka.clients.producer.ProducerRecord +import org.scalatest.BeforeAndAfterAll +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +import java.util.UUID +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.FiniteDuration + +class VaultSecretProviderIT + extends AnyFunSuite + with SecretProviderContainerPerSuite + with Matchers + with BeforeAndAfterAll { + + lazy val secretTtlSeconds = 5 + + lazy val container: VaultContainer = + new VaultContainer( + LeaseInfo(secretTtlSeconds, secretTtlSeconds).some, + ).withNetwork(network) + + override def modules: Seq[String] = Seq("secret-provider", "test-sink") + + val topicName = "vaultVandal" + val connectorName = "testSink" + override def beforeAll(): Unit = { + container.start() + super.beforeAll() + } + + override def afterAll(): Unit = { + super.afterAll() + container.stop() + } + + test("does not fail with secret TTLs") { + (for { + _ <- container.configureMount(secretTtlSeconds) + initialSecret <- container.rotateSecret(secretTtlSeconds) + latestSecretRef <- Ref.of[IO, String](initialSecret) + _ <- secretUpdater(latestSecretRef) + _ <- startConnector() + } yield ()).unsafeRunSync() + } + + private def startConnector(): IO[Unit] = + makeStringStringProducerResource().use { prod => + withConnectorResource(sinkConfig()).use(_ => writeRecords(prod)) + } + + private def writeRecords(prod: KafkaProducer[String, String]): IO[Unit] = + IO { + (1 to 100).foreach { i => + writeRecord(prod, i) + Thread.sleep(250) + } + } + + private def writeRecord( + producer: KafkaProducer[String, String], + i: Int, + ): Unit = { + val record: ProducerRecord[String, String] = + new ProducerRecord[String, String]( + topicName, + s"${i}_${UUID.randomUUID().toString}", + ) + producer.send(record).get + producer.flush() + } + + private def withConnectorResource( + connectorConfig: ConnectorConfiguration, + timeoutSeconds: Long = 10L, + )( + implicit + kafkaConnectClient: KafkaConnectClient, + ): Resource[IO, String] = + Resource.make( + IO { + val connectorName = connectorConfig.name + kafkaConnectClient.registerConnector(connectorConfig) + kafkaConnectClient.configureLogging("org.apache.kafka.connect.runtime.WorkerConfigTransformer", "DEBUG") + kafkaConnectClient.waitConnectorInRunningState( + connectorName, + timeoutSeconds, + ) + connectorName + }, + )(connectorName => IO(kafkaConnectClient.deleteConnector(connectorName))) + + private def secretUpdater(ref: Ref[IO, String]): IO[FiberIO[Unit]] = + (for { + _ <- IO.sleep(FiniteDuration(secretTtlSeconds, TimeUnit.SECONDS)) + rotated <- container.rotateSecret(secretTtlSeconds) + _ <- ref.set(rotated) + updateAgain <- secretUpdater(ref) + _ <- updateAgain.join + } yield ()).start + + private def sinkConfig(): ConnectorConfiguration = + ConnectorConfiguration( + connectorName, + Map( + "name" -> StringCnfVal(connectorName), + "connector.class" -> StringCnfVal( + "io.lenses.connect.secrets.test.TestSinkConnector", + ), + "topics" -> StringCnfVal(topicName), + "key.converter" -> StringCnfVal( + "org.apache.kafka.connect.storage.StringConverter", + ), + "value.converter" -> StringCnfVal( + "org.apache.kafka.connect.storage.StringConverter", + ), + "test.sink.vault.host" -> StringCnfVal(container.networkVaultAddress), + "test.sink.vault.token" -> StringCnfVal(container.vaultToken), + "test.sink.secret.path" -> StringCnfVal(vaultSecretPath), + "test.sink.secret.key" -> StringCnfVal(vaultSecretKey), + "test.sink.secret.value" -> StringCnfVal( + s"$${vault:$vaultSecretPath:$vaultSecretKey}", + ), + ), + ) + + override def getConfigProviders(): Option[ConfigProviders] = + ConfigProviders( + Seq( + ConfigProvider( + "vault", + "io.lenses.connect.secrets.providers.VaultSecretProvider", + Map( + "vault.addr" -> container.networkVaultAddress, + "vault.engine.version" -> "2", + "vault.auth.method" -> "token", + "vault.token" -> container.vaultToken, + "default.ttl" -> "30000", + "file.write" -> "false", + ), + ), + ), + ).some +} diff --git a/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/KafkaConnectContainer.scala b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/KafkaConnectContainer.scala new file mode 100644 index 0000000..c1daf85 --- /dev/null +++ b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/KafkaConnectContainer.scala @@ -0,0 +1,102 @@ +package io.lenses.connect.secrets.testcontainers + +import com.github.dockerjava.api.model.Ulimit +import io.lenses.connect.secrets.testcontainers.KafkaConnectContainer.defaultNetworkAlias +import io.lenses.connect.secrets.testcontainers.KafkaConnectContainer.defaultRestPort +import io.lenses.connect.secrets.testcontainers.connect.ConfigProviders +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.KafkaContainer +import org.testcontainers.containers.wait.strategy.Wait +import org.testcontainers.utility.DockerImageName + +import java.time.Duration +import scala.jdk.CollectionConverters.MapHasAsJava + +class KafkaConnectContainer( + dockerImage: String, + networkAlias: String = defaultNetworkAlias, + restPort: Int = defaultRestPort, + connectPluginPath: Map[String, String] = Map.empty, + kafkaContainer: KafkaContainer, + maybeConfigProviders: Option[ConfigProviders], +) extends GenericContainer[KafkaConnectContainer](DockerImageName.parse(dockerImage)) { + require(kafkaContainer != null, "You must define the kafka container") + + withNetwork(kafkaContainer.getNetwork) + withNetworkAliases(networkAlias) + withExposedPorts(restPort) + waitingFor( + Wait + .forHttp("/connectors") + .forPort(restPort) + .forStatusCode(200), + ).withStartupTimeout(Duration.ofSeconds(120)) + withCreateContainerCmdModifier { cmd => + val _ = + cmd.getHostConfig.withUlimits(Array(new Ulimit("nofile", 65536L, 65536L))) + } + + connectPluginPath.foreach { + case (k, v) => withFileSystemBind(v, s"/usr/share/plugins/$k") + } + + withEnv("CONNECT_KEY_CONVERTER_SCHEMAS_ENABLE", "false") + withEnv("CONNECT_VALUE_CONVERTER_SCHEMAS_ENABLE", "false") + withEnv( + "CONNECT_KEY_CONVERTER", + "org.apache.kafka.connect.storage.StringConverter", + ) + withEnv( + "CONNECT_VALUE_CONVERTER", + "org.apache.kafka.connect.storage.StringConverter", + ) + + withEnv("CONNECT_REST_ADVERTISED_HOST_NAME", networkAlias) + withEnv("CONNECT_REST_PORT", restPort.toString) + withEnv("CONNECT_BOOTSTRAP_SERVERS", kafkaContainer.bootstrapServers) + withEnv("CONNECT_GROUP_ID", "io/lenses/connect/secrets/integration/connect") + withEnv("CONNECT_CONFIG_STORAGE_TOPIC", "connect_config") + withEnv("CONNECT_OFFSET_STORAGE_TOPIC", "connect_offset") + withEnv("CONNECT_STATUS_STORAGE_TOPIC", "connect_status") + withEnv("CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR", "1") + withEnv("CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR", "1") + withEnv("CONNECT_STATUS_STORAGE_REPLICATION_FACTOR", "1") + withEnv( + "CONNECT_PLUGIN_PATH", + "/usr/share/java,/usr/share/confluent-hub-components,/usr/share/plugins", + ) + + maybeConfigProviders.map(cpc => withEnv(cpc.toEnvMap.asJava)) + + lazy val hostNetwork = new HostNetwork() + + class HostNetwork { + def restEndpointUrl: String = s"http://$getHost:${getMappedPort(restPort)}" + } + +} +object KafkaConnectContainer { + private val dockerImage = + DockerImageName.parse("confluentinc/cp-kafka-connect") + private val defaultConfluentPlatformVersion: String = + sys.env.getOrElse("CONFLUENT_VERSION", "7.3.1") + private val defaultNetworkAlias = "connect" + private val defaultRestPort = 8083 + + def apply( + confluentPlatformVersion: String = defaultConfluentPlatformVersion, + networkAlias: String = defaultNetworkAlias, + restPort: Int = defaultRestPort, + connectPluginPathMap: Map[String, String] = Map.empty, + kafkaContainer: KafkaContainer, + maybeConfigProviders: Option[ConfigProviders], + ): KafkaConnectContainer = + new KafkaConnectContainer( + dockerImage.withTag(confluentPlatformVersion).toString, + networkAlias, + restPort, + connectPluginPathMap, + kafkaContainer, + maybeConfigProviders, + ) +} diff --git a/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/SingleContainer.scala b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/SingleContainer.scala new file mode 100644 index 0000000..12195a0 --- /dev/null +++ b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/SingleContainer.scala @@ -0,0 +1,23 @@ +package io.lenses.connect.secrets.testcontainers + +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.Network + +abstract class SingleContainer[T <: GenericContainer[_]] { + + def container: T + + def start(): Unit = container.start() + + def stop(): Unit = container.stop() + + def withNetwork(network: Network): this.type = { + container.withNetwork(network) + this + } + + def withExposedPorts(ports: Integer*): this.type = { + container.withExposedPorts(ports: _*) + this + } +} diff --git a/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/VaultContainer.scala b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/VaultContainer.scala new file mode 100644 index 0000000..58e09ff --- /dev/null +++ b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/VaultContainer.scala @@ -0,0 +1,108 @@ +package io.lenses.connect.secrets.testcontainers + +import cats.effect.IO +import cats.effect.Resource +import com.bettercloud.vault.Vault +import com.bettercloud.vault.VaultConfig +import com.bettercloud.vault.api.mounts.MountPayload +import com.bettercloud.vault.api.mounts.MountType +import com.bettercloud.vault.api.mounts.TimeToLive +import com.bettercloud.vault.response.MountResponse +import com.typesafe.scalalogging.LazyLogging +import io.lenses.connect.secrets.testcontainers.VaultContainer._ +import org.scalatest.matchers.should.Matchers +import org.testcontainers.utility.DockerImageName +import org.testcontainers.vault.{ VaultContainer => JavaVaultContainer } + +import java.util.UUID +import java.util.concurrent.TimeUnit +import scala.jdk.CollectionConverters.MapHasAsJava + +case class LeaseInfo(defaultLeaseTimeSeconds: Int, maxLeaseTimeSeconds: Int) { + def toDurString(): String = + s""""default_lease_ttl": "${defaultLeaseTimeSeconds}s", "max_lease_ttl": "${maxLeaseTimeSeconds}s",""" +} + +class VaultContainer(maybeLeaseInfo: Option[LeaseInfo] = Option.empty) + extends SingleContainer[JavaVaultContainer[_]] + with Matchers + with LazyLogging { + + private val token: String = UUID.randomUUID().toString + + override val container: JavaVaultContainer[_] = new JavaVaultContainer( + VaultDockerImageName, + ) + container.withNetworkAliases(defaultNetworkAlias) + container.withVaultToken(token) + container.withEnv( + "VAULT_LOCAL_CONFIG", //"listener": [{"tcp": { "address": "0.0.0.0:8200", "tls_disable": true}}], + s""" + |{${maybeLeaseInfo.fold("")(_.toDurString())} "ui": true}""".stripMargin, + ) + def vaultClientResource: Resource[IO, Vault] = + Resource.make( + IO( + new Vault( + new VaultConfig() + .address(container.getHttpHostAddress) + .token(token) + .engineVersion(2) + .build(), + ), + ), + )(_ => IO.unit) + + def configureMount(secretTtlSeconds: Long): IO[MountResponse] = + vaultClientResource.use(client => + IO { + logger.info("Configuring mount") + val mountPayload = new MountPayload() + mountPayload.defaultLeaseTtl(TimeToLive.of(secretTtlSeconds.intValue(), TimeUnit.SECONDS)) + client.mounts().enable(vaultRootPath, MountType.KEY_VALUE, mountPayload) + }, + ) + + def rotateSecret( + secretTtlSeconds: Long, + ): IO[String] = + vaultClientResource.use(client => + IO { + val time = System.currentTimeMillis() + val newSecretValue = s"${vaultSecretPrefix}_$time" + logger.info(s"Rotating secret to {}", newSecretValue) + client + .logical() + .write( + vaultSecretPath, + Map[String, Object]( + vaultSecretKey -> s"${vaultSecretPrefix}_$time", + "ttl" -> s"${secretTtlSeconds}s", + "created" -> Long.box(time), + "expires" -> Long.box(time + (secretTtlSeconds * 1000)), + ).asJava, + ) + newSecretValue + }, + ) + + def vaultToken = token + + def vaultAddress = container.getHttpHostAddress + + def networkVaultAddress: String = + String.format("http://%s:%s", defaultNetworkAlias, vaultPort) + +} + +object VaultContainer { + val VaultDockerImageName: DockerImageName = + DockerImageName.parse("vault").withTag("1.12.3") + def vaultRootPath = "rotate-test" // was: secret/ + def vaultSecretPath = s"$vaultRootPath/myVaultSecretPath" + def vaultSecretKey = "myVaultSecretKey" + def vaultSecretPrefix = "myVaultSecretValue" + + private val defaultNetworkAlias = "vault" + private val vaultPort: Int = 8200 +} diff --git a/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/connect/ConfigProperty.scala b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/connect/ConfigProperty.scala new file mode 100644 index 0000000..2d7d737 --- /dev/null +++ b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/connect/ConfigProperty.scala @@ -0,0 +1,21 @@ +package io.lenses.connect.secrets.testcontainers.connect + +trait CnfVal[T <: Any] { + def get: T +} + +case class StringCnfVal(s: String) extends CnfVal[String] { + override def get: String = s +} + +case class IntCnfVal(s: Int) extends CnfVal[Int] { + override def get: Int = s +} + +case class LongCnfVal(s: Long) extends CnfVal[Long] { + override def get: Long = s +} + +case class BooleanCnfVal(s: Boolean) extends CnfVal[Boolean] { + override def get: Boolean = s +} diff --git a/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/connect/ConfigProviderConfig.scala b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/connect/ConfigProviderConfig.scala new file mode 100644 index 0000000..81104d3 --- /dev/null +++ b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/connect/ConfigProviderConfig.scala @@ -0,0 +1,25 @@ +package io.lenses.connect.secrets.testcontainers.connect + +case class ConfigProviders(configProviders: Seq[ConfigProvider]) { + def toEnvMap: Map[String, String] = { + val providersEnv = "CONNECT_CONFIG_PROVIDERS" -> configProviders.map(_.name).mkString(",") + (providersEnv +: configProviders.flatMap(_.toEnvMap)).toMap + } +} + +case class ConfigProvider( + name: String, + `class`: String, + props: Map[String, String], +) { + private val classProp = s"CONNECT_CONFIG_PROVIDERS_${name.toUpperCase}_CLASS" + private val paramPrefix = s"CONNECT_CONFIG_PROVIDERS.${name.toUpperCase}_PARAM_" + + def toEnvMap: Map[String, String] = { + val classEnv = classProp -> `class` + val envs = props.map { + case (pk, pv) => paramPrefix + pk.replace(".", "_").toUpperCase -> pv + } + envs + classEnv + } +} diff --git a/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/connect/ConnectorConfiguration.scala b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/connect/ConnectorConfiguration.scala new file mode 100644 index 0000000..8b7b703 --- /dev/null +++ b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/connect/ConnectorConfiguration.scala @@ -0,0 +1,27 @@ +package io.lenses.connect.secrets.testcontainers.connect + +import org.json4s.DefaultFormats +import org.json4s.native.Serialization + +case class ConnectorConfiguration( + name: String, + config: Map[String, CnfVal[_]], +) { + + implicit val formats: DefaultFormats.type = DefaultFormats + + def toJson(): String = { + val mergedConfigMap = config + ("tasks.max" -> IntCnfVal(1)) + Serialization.write( + Map[String, Any]( + "name" -> name, + "config" -> transformConfigMap(mergedConfigMap), + ), + ) + } + + private def transformConfigMap( + originalMap: Map[String, CnfVal[_]], + ): Map[String, Any] = originalMap.view.mapValues(_.get).toMap + +} diff --git a/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/connect/KafkaConnectClient.scala b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/connect/KafkaConnectClient.scala new file mode 100644 index 0000000..c5bbc0e --- /dev/null +++ b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/connect/KafkaConnectClient.scala @@ -0,0 +1,146 @@ +package io.lenses.connect.secrets.testcontainers.connect + +import com.typesafe.scalalogging.StrictLogging +import io.lenses.connect.secrets.testcontainers.KafkaConnectContainer +import io.lenses.connect.secrets.testcontainers.connect.KafkaConnectClient.ConnectorStatus +import org.apache.http.HttpHeaders +import org.apache.http.HttpResponse +import org.apache.http.client.HttpClient +import org.apache.http.client.methods.HttpDelete +import org.apache.http.client.methods.HttpGet +import org.apache.http.client.methods.HttpPost +import org.apache.http.client.methods.HttpPut +import org.apache.http.entity.StringEntity +import org.apache.http.impl.client.HttpClientBuilder +import org.apache.http.message.BasicHeader +import org.apache.http.util.EntityUtils +import org.json4s.DefaultFormats +import org.json4s._ +import org.json4s.native.JsonMethods._ +import org.scalatest.concurrent.Eventually +import org.testcontainers.shaded.org.awaitility.Awaitility.await + +import java.util.concurrent.TimeUnit +import scala.jdk.CollectionConverters._ + +class KafkaConnectClient(kafkaConnectContainer: KafkaConnectContainer) extends StrictLogging with Eventually { + + implicit val formats: DefaultFormats.type = DefaultFormats + + val httpClient: HttpClient = { + val acceptHeader = new BasicHeader(HttpHeaders.ACCEPT, "application/json") + val contentHeader = + new BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + HttpClientBuilder.create + .setDefaultHeaders(List(acceptHeader, contentHeader).asJava) + .build() + } + + def configureLogging(loggerFQCN: String, loggerLevel: String): Unit = { + val entity = new StringEntity(s"""{ "level": "${loggerLevel.toUpperCase}" }""") + entity.setContentType("application/json") + val httpPut = new HttpPut( + s"${kafkaConnectContainer.hostNetwork.restEndpointUrl}/admin/loggers/$loggerFQCN", + ) + httpPut.setEntity(entity) + val response = httpClient.execute(httpPut) + checkRequestSuccessful(response) + } + + def registerConnector( + connector: ConnectorConfiguration, + timeoutSeconds: Long = 10L, + ): Unit = { + val httpPost = new HttpPost( + s"${kafkaConnectContainer.hostNetwork.restEndpointUrl}/connectors", + ) + val entity = new StringEntity(connector.toJson()) + httpPost.setEntity(entity) + val response = httpClient.execute(httpPost) + checkRequestSuccessful(response) + EntityUtils.consume(response.getEntity) + await + .atMost(timeoutSeconds, TimeUnit.SECONDS) + .until(() => isConnectorConfigured(connector.name)) + } + + def deleteConnector( + connectorName: String, + timeoutSeconds: Long = 10L, + ): Unit = { + val httpDelete = + new HttpDelete( + s"${kafkaConnectContainer.hostNetwork.restEndpointUrl}/connectors/$connectorName", + ) + val response = httpClient.execute(httpDelete) + checkRequestSuccessful(response) + EntityUtils.consume(response.getEntity) + await + .atMost(timeoutSeconds, TimeUnit.SECONDS) + .until(() => !this.isConnectorConfigured(connectorName)) + } + + def getConnectorStatus(connectorName: String): ConnectorStatus = { + val httpGet = new HttpGet( + s"${kafkaConnectContainer.hostNetwork.restEndpointUrl}/connectors/$connectorName/status", + ) + val response = httpClient.execute(httpGet) + checkRequestSuccessful(response) + val strResponse = EntityUtils.toString(response.getEntity) + logger.info(s"Connector status: $strResponse") + parse(strResponse).extract[ConnectorStatus] + } + + def isConnectorConfigured(connectorName: String): Boolean = { + val httpGet = new HttpGet( + s"${kafkaConnectContainer.hostNetwork.restEndpointUrl}/connectors/$connectorName", + ) + val response = httpClient.execute(httpGet) + EntityUtils.consume(response.getEntity) + response.getStatusLine.getStatusCode == 200 + } + + def waitConnectorInRunningState( + connectorName: String, + timeoutSeconds: Long = 10L, + ): Unit = + await + .atMost(timeoutSeconds, TimeUnit.SECONDS) + .until { () => + try { + val connectorState: String = + getConnectorStatus(connectorName).connector.state + logger.info("Connector State: {}", connectorState) + connectorState.equals("RUNNING") + } catch { + case e: Throwable => + logger.error("Connector Throwable: {}", e) + false + } + } + + def checkRequestSuccessful(response: HttpResponse): Unit = + if (!isSuccess(response.getStatusLine.getStatusCode)) { + throw new IllegalStateException( + s"Http request failed with response: ${EntityUtils.toString(response.getEntity)}", + ) + } + + def isSuccess(code: Int): Boolean = code / 100 == 2 +} + +object KafkaConnectClient { + case class ConnectorStatus( + name: String, + connector: Connector, + tasks: Seq[Tasks], + `type`: String, + ) + case class Connector(state: String, worker_id: String) + case class Tasks( + id: Int, + state: String, + worker_id: String, + trace: Option[String], + ) +} diff --git a/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/scalatest/SecretProviderContainerPerSuite.scala b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/scalatest/SecretProviderContainerPerSuite.scala new file mode 100644 index 0000000..8a936d7 --- /dev/null +++ b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/scalatest/SecretProviderContainerPerSuite.scala @@ -0,0 +1,186 @@ +package io.lenses.connect.secrets.testcontainers.scalatest + +import cats.effect.IO +import cats.effect.Resource +import com.typesafe.scalalogging.LazyLogging +import io.lenses.connect.secrets.testcontainers.connect.ConfigProviders +import io.lenses.connect.secrets.testcontainers.connect.KafkaConnectClient +import io.lenses.connect.secrets.testcontainers.KafkaConnectContainer +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.apache.kafka.clients.consumer.KafkaConsumer +import org.apache.kafka.clients.producer.KafkaProducer +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.common.serialization.StringDeserializer +import org.apache.kafka.common.serialization.StringSerializer +import org.scalatest.BeforeAndAfterAll +import org.scalatest.TestSuite +import org.scalatest.concurrent.Eventually +import org.scalatest.time.Minute +import org.scalatest.time.Span +import org.testcontainers.containers.KafkaContainer +import org.testcontainers.containers.Network +import org.testcontainers.containers.output.Slf4jLogConsumer +import org.testcontainers.utility.DockerImageName + +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.time.Duration +import java.util +import java.util.Properties +import java.util.UUID +import java.util.stream.Collectors +import scala.collection.mutable.ListBuffer +import scala.util.Try + +trait SecretProviderContainerPerSuite extends BeforeAndAfterAll with Eventually with LazyLogging { this: TestSuite => + + def getConfigProviders(): Option[ConfigProviders] = Option.empty + + override implicit def patienceConfig: PatienceConfig = + PatienceConfig(timeout = Span(1, Minute)) + + private val confluentPlatformVersion: String = { + val (vers, from) = sys.env.get("CONFLUENT_VERSION") match { + case Some(value) => (value, "env") + case None => ("7.3.1", "default") + } + logger.info("Selected confluent version {} from {}", vers, from) + vers + } + + val network: Network = Network.SHARED + + def modules: Seq[String] + + lazy val kafkaContainer: KafkaContainer = + new KafkaContainer( + DockerImageName.parse(s"confluentinc/cp-kafka:$confluentPlatformVersion"), + ).withNetwork(network) + .withNetworkAliases("kafka") + .withLogConsumer(new Slf4jLogConsumer(logger.underlying)) + + lazy val kafkaConnectContainer: KafkaConnectContainer = { + KafkaConnectContainer( + kafkaContainer = kafkaContainer, + connectPluginPathMap = connectPluginPath(), + maybeConfigProviders = getConfigProviders(), + ).withNetwork(network) + .withLogConsumer(new Slf4jLogConsumer(logger.underlying)) + } + + implicit lazy val kafkaConnectClient: KafkaConnectClient = + new KafkaConnectClient(kafkaConnectContainer) + + override def beforeAll(): Unit = { + kafkaContainer.start() + kafkaConnectContainer.start() + super.beforeAll() + } + + override def afterAll(): Unit = + try super.afterAll() + finally { + kafkaConnectContainer.stop() + kafkaContainer.stop() + } + + def connectPluginPath(): Map[String, String] = { + + val dir: String = detectUserOrGithubDir + + modules.map { module: String => + val regex = s".*$module.*.jar" + val files: util.List[Path] = Files + .find( + Paths.get( + String.join( + File.separator, + dir, + module, + "target", + ), + ), + 3, + (p, _) => p.toFile.getName.matches(regex), + ) + .collect(Collectors.toList()) + if (files.isEmpty) + throw new RuntimeException( + s"""Please run `sbt "project $module" assembly""", + ) + module -> files.get(0).getParent.toString + }.toMap + + } + + private def detectUserOrGithubDir = { + val userDir = sys.props("user.dir") + val githubWs = Try(Option(sys.env("GITHUB_WORKSPACE"))).toOption.flatten + + logger.info("userdir: {} githubWs: {}", userDir, githubWs) + + githubWs.getOrElse(userDir) + } + + def makeStringStringProducerResource(): Resource[IO, KafkaProducer[String, String]] = + makeProducerResource(classOf[StringSerializer], classOf[StringSerializer]) + + def makeProducerResource[K, V]( + keySer: Class[_], + valueSer: Class[_], + ): Resource[IO, KafkaProducer[K, V]] = { + val props = new Properties + props.put( + ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, + kafkaContainer.getBootstrapServers, + ) + props.put(ProducerConfig.ACKS_CONFIG, "all") + props.put(ProducerConfig.RETRIES_CONFIG, 0) + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, keySer) + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, valueSer) + Resource.make(IO(new KafkaProducer[K, V](props)))(r => IO(r.close())) + } + + def createConsumer(): KafkaConsumer[String, String] = { + val props = new Properties + props.put( + ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, + kafkaContainer.getBootstrapServers, + ) + props.put(ConsumerConfig.GROUP_ID_CONFIG, "cg-" + UUID.randomUUID()) + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest") + new KafkaConsumer[String, String]( + props, + new StringDeserializer(), + new StringDeserializer(), + ) + } + + /** + * Drain a kafka topic. + * + * @param consumer the kafka consumer + * @param expectedRecordCount the expected record count + * @tparam K the key type + * @tparam V the value type + * @return the records + */ + def drain[K, V]( + consumer: KafkaConsumer[K, V], + expectedRecordCount: Int, + ): List[ConsumerRecord[K, V]] = { + val allRecords = ListBuffer[ConsumerRecord[K, V]]() + + eventually { + consumer + .poll(Duration.ofMillis(50)) + .iterator() + .forEachRemaining(e => allRecords :+ e) + assert(allRecords.size == expectedRecordCount) + } + allRecords.toList + } +} diff --git a/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/testcontainers.scala b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/testcontainers.scala new file mode 100644 index 0000000..4ee9a00 --- /dev/null +++ b/secret-provider/src/it/scala/io/lenses/connect/secrets/testcontainers/testcontainers.scala @@ -0,0 +1,13 @@ +package io.lenses.connect.secrets + +import org.testcontainers.containers.KafkaContainer + +package object testcontainers { + + implicit class KafkaContainerOps(kafkaContainer: KafkaContainer) { + val kafkaPort = 9092 + + def bootstrapServers: String = + s"PLAINTEXT://${kafkaContainer.getNetworkAliases.get(0)}:$kafkaPort" + } +} diff --git a/secret-provider/src/main/scala/io/lenses/connect/secrets/async/AsyncFunctionLoop.scala b/secret-provider/src/main/scala/io/lenses/connect/secrets/async/AsyncFunctionLoop.scala index 5952fb3..f0cd8ab 100644 --- a/secret-provider/src/main/scala/io/lenses/connect/secrets/async/AsyncFunctionLoop.scala +++ b/secret-provider/src/main/scala/io/lenses/connect/secrets/async/AsyncFunctionLoop.scala @@ -41,7 +41,7 @@ class AsyncFunctionLoop(interval: Duration, description: String)(thunk: => Unit) } catch { case _: InterruptedException => case t: Throwable => - logger.warn("Failed to renew the Kerberos ticket", t) + logger.warn(s"Failed to run function $description", t) failure.incrementAndGet() } } diff --git a/secret-provider/src/main/scala/io/lenses/connect/secrets/cache/TtlCache.scala b/secret-provider/src/main/scala/io/lenses/connect/secrets/cache/TtlCache.scala new file mode 100644 index 0000000..28984b9 --- /dev/null +++ b/secret-provider/src/main/scala/io/lenses/connect/secrets/cache/TtlCache.scala @@ -0,0 +1,40 @@ +package io.lenses.connect.secrets.cache + +import com.typesafe.scalalogging.LazyLogging + +import java.time.Clock +import scala.collection.concurrent.TrieMap + +class TtlCache[V](fnGetMissingValue: String => Either[Throwable, ValueWithTtl[V]])(implicit clock: Clock) + extends LazyLogging { + + private val cache = TrieMap[String, ValueWithTtl[V]]() + + def cachingWithTtl( + key: String, + fnCondition: V => Boolean = _ => true, + fnFilterReturnKeys: ValueWithTtl[V] => ValueWithTtl[V] = identity, + ): Either[Throwable, ValueWithTtl[V]] = { + get(key, + Seq( + v => v.isAlive, + v => fnCondition(v.value), + ), + ).fold(fnGetMissingValue(key).map(put(key, _)))(Right(_)) + }.map(fnFilterReturnKeys) + + private def get( + key: String, + fnConditions: Seq[ValueWithTtl[V] => Boolean] = Seq(_ => true), + ): Option[ValueWithTtl[V]] = + cache.get(key).filter(value => fnConditions.forall(_(value))) + + private def put( + key: String, + value: ValueWithTtl[V], + ): ValueWithTtl[V] = { + cache.put(key, value) + value + } + +} diff --git a/secret-provider/src/main/scala/io/lenses/connect/secrets/cache/ValueWithTtl.scala b/secret-provider/src/main/scala/io/lenses/connect/secrets/cache/ValueWithTtl.scala new file mode 100644 index 0000000..e29dda7 --- /dev/null +++ b/secret-provider/src/main/scala/io/lenses/connect/secrets/cache/ValueWithTtl.scala @@ -0,0 +1,37 @@ +package io.lenses.connect.secrets.cache + +import java.time.Clock +import java.time.OffsetDateTime +import java.time.temporal.ChronoUnit +import scala.concurrent.duration.Duration +import scala.concurrent.duration.MILLISECONDS + +case class Ttl(originalTtl: Duration, expiry: OffsetDateTime) { + + def ttlRemaining(implicit clock: Clock): Duration = + Duration(expiry.toInstant.toEpochMilli - clock.millis(), MILLISECONDS) + + def isAlive(implicit clock: Clock) = expiry.toInstant.isAfter(clock.instant()) + +} + +object Ttl { + def apply(ttl: Option[Duration], defaultTtl: Option[Duration])(implicit clock: Clock): Option[Ttl] = + ttl.orElse(defaultTtl).map(finalTtl => Ttl(finalTtl, ttlToExpiry(finalTtl))) + + def ttlToExpiry(ttl: Duration)(implicit clock: Clock): OffsetDateTime = { + val offset = clock.getZone.getRules.getOffset(clock.instant()) + val offsetDateTime = clock.instant().plus(ttl.toMillis, ChronoUnit.MILLIS).atOffset(offset) + offsetDateTime + } + +} + +case class ValueWithTtl[V](ttlAndExpiry: Option[Ttl], value: V) { + def isAlive(implicit clock: Clock): Boolean = ttlAndExpiry.exists(_.isAlive) +} + +object ValueWithTtl { + def apply[V](ttl: Option[Duration], defaultTtl: Option[Duration], value: V)(implicit clock: Clock): ValueWithTtl[V] = + ValueWithTtl(Ttl(ttl, defaultTtl), value) +} diff --git a/secret-provider/src/main/scala/io/lenses/connect/secrets/config/VaultProviderConfig.scala b/secret-provider/src/main/scala/io/lenses/connect/secrets/config/VaultProviderConfig.scala index 33ed9e6..1fe1a84 100644 --- a/secret-provider/src/main/scala/io/lenses/connect/secrets/config/VaultProviderConfig.scala +++ b/secret-provider/src/main/scala/io/lenses/connect/secrets/config/VaultProviderConfig.scala @@ -8,6 +8,8 @@ package io.lenses.connect.secrets.config import io.lenses.connect.secrets.connect.FILE_DIR import io.lenses.connect.secrets.connect.FILE_DIR_DESC +import io.lenses.connect.secrets.connect.WRITE_FILES +import io.lenses.connect.secrets.connect.WRITE_FILES_DESC import org.apache.kafka.common.config.ConfigDef.Importance import org.apache.kafka.common.config.ConfigDef.Type import org.apache.kafka.common.config.AbstractConfig @@ -76,6 +78,9 @@ object VaultProviderConfig { val TOKEN_RENEWAL: String = "token.renewal.ms" val TOKEN_RENEWAL_DEFAULT: Int = 600000 + val SECRET_DEFAULT_TTL = "secret.default.ttl" + val SECRET_DEFAULT_TTL_DEFAULT = 0L + val config: ConfigDef = new ConfigDef() .define( VAULT_ADDR, @@ -354,6 +359,13 @@ object VaultProviderConfig { Importance.MEDIUM, FILE_DIR_DESC, ) + .define( + WRITE_FILES, + Type.BOOLEAN, + false, + Importance.MEDIUM, + WRITE_FILES_DESC, + ) .define( TOKEN_RENEWAL, Type.INT, @@ -361,5 +373,13 @@ object VaultProviderConfig { Importance.MEDIUM, "The time in milliseconds to renew the Vault token", ) + .define( + SECRET_DEFAULT_TTL, + Type.LONG, + SECRET_DEFAULT_TTL_DEFAULT, + Importance.MEDIUM, + "Default TTL to apply in case a secret has no TTL", + ) + } case class VaultProviderConfig(props: util.Map[String, _]) extends AbstractConfig(VaultProviderConfig.config, props) diff --git a/secret-provider/src/main/scala/io/lenses/connect/secrets/config/VaultProviderSettings.scala b/secret-provider/src/main/scala/io/lenses/connect/secrets/config/VaultProviderSettings.scala index ae934cb..694c252 100644 --- a/secret-provider/src/main/scala/io/lenses/connect/secrets/config/VaultProviderSettings.scala +++ b/secret-provider/src/main/scala/io/lenses/connect/secrets/config/VaultProviderSettings.scala @@ -9,11 +9,14 @@ package io.lenses.connect.secrets.config import com.typesafe.scalalogging.StrictLogging import io.lenses.connect.secrets.config.AbstractConfigExtensions._ import io.lenses.connect.secrets.config.VaultAuthMethod.VaultAuthMethod +import io.lenses.connect.secrets.config.VaultProviderConfig.SECRET_DEFAULT_TTL import io.lenses.connect.secrets.config.VaultProviderConfig.TOKEN_RENEWAL import io.lenses.connect.secrets.connect._ +import io.lenses.connect.secrets.io.FileWriterOnce import org.apache.kafka.common.config.types.Password import org.apache.kafka.connect.errors.ConnectException +import java.nio.file.Paths import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration._ import scala.io.Source @@ -37,28 +40,37 @@ case class K8s(role: String, jwt: Password) case class Cert(mount: String) case class Github(token: Password, mount: String) +case class FileWriterOptions( + fileDir: String, +) { + def createFileWriter(path: String): FileWriterOnce = + new FileWriterOnce(Paths.get(fileDir, path)) + +} + case class VaultSettings( - addr: String, - namespace: String, - token: Password, - authMode: VaultAuthMethod, - keystoreLoc: String, - keystorePass: Password, - truststoreLoc: String, - pem: String, - clientPem: String, - engineVersion: Int = 2, - appRole: Option[AppRole], - awsIam: Option[AwsIam], - gcp: Option[Gcp], - jwt: Option[Jwt], - userPass: Option[UserPass], - ldap: Option[Ldap], - k8s: Option[K8s], - cert: Option[Cert], - github: Option[Github], - fileDir: String, - tokenRenewal: FiniteDuration, + addr: String, + namespace: String, + token: Password, + authMode: VaultAuthMethod, + keystoreLoc: String, + keystorePass: Password, + truststoreLoc: String, + pem: String, + clientPem: String, + engineVersion: Int = 2, + appRole: Option[AppRole], + awsIam: Option[AwsIam], + gcp: Option[Gcp], + jwt: Option[Jwt], + userPass: Option[UserPass], + ldap: Option[Ldap], + k8s: Option[K8s], + cert: Option[Cert], + github: Option[Github], + tokenRenewal: FiniteDuration, + fileWriterOpts: Option[FileWriterOptions], + defaultTtl: Option[Duration], ) object VaultSettings extends StrictLogging { @@ -109,8 +121,6 @@ object VaultSettings extends StrictLogging { if (authMode.equals(VaultAuthMethod.GITHUB)) Some(getGitHub(config)) else None - val fileDir = config.getString(FILE_DIR) - val tokenRenewal = config.getInt(TOKEN_RENEWAL).toInt.milliseconds VaultSettings( addr = addr, @@ -132,8 +142,11 @@ object VaultSettings extends StrictLogging { k8s = k8s, cert = cert, github = github, - fileDir = fileDir, tokenRenewal = tokenRenewal, + fileWriterOpts = Option.when(config.getBoolean(WRITE_FILES)) { + FileWriterOptions(config.getString(FILE_DIR)) + }, + defaultTtl = Option(config.getLong(SECRET_DEFAULT_TTL).toLong).filterNot(_ == 0L).map(Duration(_, MILLISECONDS)), ) } diff --git a/secret-provider/src/main/scala/io/lenses/connect/secrets/package.scala b/secret-provider/src/main/scala/io/lenses/connect/secrets/package.scala index 206477b..21bb652 100644 --- a/secret-provider/src/main/scala/io/lenses/connect/secrets/package.scala +++ b/secret-provider/src/main/scala/io/lenses/connect/secrets/package.scala @@ -32,6 +32,14 @@ package object connect extends StrictLogging { | pattern /file.dir/[path|keyvault]/key |""".stripMargin + val WRITE_FILES: String = "file.write" + val WRITE_FILES_DESC: String = + """ + | Boolean flag whether to write secrets to disk. Defaults + | to false. Should be set to true when writing Java + | keystores etc. + |""".stripMargin + object AuthMode extends Enumeration { type AuthMode = Value val DEFAULT, CREDENTIALS = Value diff --git a/secret-provider/src/main/scala/io/lenses/connect/secrets/providers/VaultSecretProvider.scala b/secret-provider/src/main/scala/io/lenses/connect/secrets/providers/VaultSecretProvider.scala index 2ab1fbb..ba00a71 100644 --- a/secret-provider/src/main/scala/io/lenses/connect/secrets/providers/VaultSecretProvider.scala +++ b/secret-provider/src/main/scala/io/lenses/connect/secrets/providers/VaultSecretProvider.scala @@ -6,22 +6,26 @@ package io.lenses.connect.secrets.providers +import com.bettercloud.vault.Vault +import com.bettercloud.vault.response.LogicalResponse +import io.lenses.connect.secrets.async.AsyncFunctionLoop +import io.lenses.connect.secrets.cache.TtlCache +import io.lenses.connect.secrets.cache.ValueWithTtl import io.lenses.connect.secrets.config.VaultProviderConfig import io.lenses.connect.secrets.config.VaultSettings import io.lenses.connect.secrets.connect._ -import com.bettercloud.vault.Vault -import io.lenses.connect.secrets.async.AsyncFunctionLoop -import io.lenses.connect.secrets.io.FileWriterOnce +import io.lenses.connect.secrets.utils.ConfigDataBuilder import io.lenses.connect.secrets.utils.EncodingAndId +import io.lenses.connect.secrets.utils.ExceptionUtils.failWithEx import org.apache.kafka.common.config.ConfigData import org.apache.kafka.common.config.provider.ConfigProvider -import org.apache.kafka.connect.errors.ConnectException -import java.nio.file.Paths -import java.time.OffsetDateTime +import java.time.Clock import java.util -import scala.collection.mutable -import scala.jdk.CollectionConverters._ +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.Duration +import scala.jdk.CollectionConverters.MapHasAsScala +import scala.jdk.CollectionConverters.SetHasAsScala import scala.util.Failure import scala.util.Success import scala.util.Try @@ -31,18 +35,21 @@ class VaultSecretProvider() extends ConfigProvider with VaultHelper { private var settings: VaultSettings = _ private var vaultClient: Option[Vault] = None private var tokenRenewal: Option[AsyncFunctionLoop] = None - private val cache = - mutable.Map.empty[String, (Option[OffsetDateTime], ConfigData)] + + private implicit val clock: Clock = Clock.systemDefaultZone() + private val cache = new TtlCache[Map[String, String]](k => getSecretValue(k)) + def getClient: Option[Vault] = vaultClient // configure the vault client override def configure(configs: util.Map[String, _]): Unit = { settings = VaultSettings(VaultProviderConfig(configs)) vaultClient = Some(createClient(settings)) - val renewalLoop = + val renewalLoop = { new AsyncFunctionLoop(settings.tokenRenewal, "Vault Token Renewal")( renewToken(), ) + } tokenRenewal = Some(renewalLoop) renewalLoop.start() } @@ -55,126 +62,83 @@ class VaultSecretProvider() extends ConfigProvider with VaultHelper { // lookup secrets at a path override def get(path: String): ConfigData = { - val (expiry, data) = cache.get(path) match { - case Some((expiresAt, data)) => - // we have all the keys and are before the expiry - val now = OffsetDateTime.now() - - if (expiresAt.getOrElse(now.plusSeconds(1)).isAfter(now)) { - logger.info("Fetching secrets from cache") - (expiresAt, data) - } else { - // missing some or expired so reload - getSecretsAndExpiry(getSecrets(path)) - } - - case None => - getSecretsAndExpiry(getSecrets(path)) - } - - expiry.foreach(exp => logger.info(s"Min expiry for TTL set to [${exp.toString}]")) - cache += (path -> (expiry, data)) - data + logger.debug(" -> VaultSecretProvider.get(path: {})", path) + val sec = cache + .cachingWithTtl( + path, + ) + .fold( + ex => throw ex, + ConfigDataBuilder(_), + ) + logger.debug(" <- VaultSecretProvider.get(path: {}, ttl: {})", path, sec.ttl()) + sec } // get secret keys at a path override def get(path: String, keys: util.Set[String]): ConfigData = { - - val (expiry, data) = cache.get(path) match { - case Some((expiresAt, data)) => - // we have all the keys and are before the expiry - val now = OffsetDateTime.now() - - if ( - keys.asScala.subsetOf(data.data().asScala.keySet) && expiresAt - .getOrElse(now.plusSeconds(1)) - .isAfter(now) - ) { - logger.info("Fetching secrets from cache") - ( - expiresAt, - new ConfigData( - data - .data() - .asScala - .view - .filter { - case (k, _) => keys.contains(k) - } - .toMap - .asJava, - data.ttl(), - ), - ) - } else { - // missing some or expired so reload - getSecretsAndExpiry(getSecrets(path).view.filter { - case (k, _) => keys.contains(k) - }.toMap) - } - - case None => - getSecretsAndExpiry(getSecrets(path).view.filter { - case (k, _) => keys.contains(k) - }.toMap) - } - - expiry.foreach(exp => logger.info(s"Min expiry for TTL set to [${exp.toString}]")) - cache += (path -> (expiry, data)) - data + logger.debug(" -> VaultSecretProvider.get(path: {}, keys: {})", path, keys.asScala) + val sec = cache.cachingWithTtl( + path, + fnCondition = s => keys.asScala.subsetOf(s.keySet), + fnFilterReturnKeys = filter(_, keys.asScala.toSet), + ).fold( + ex => throw ex, + ConfigDataBuilder(_), + ) + logger.debug(" <- VaultSecretProvider.get(path: {}, keys: {}, ttl: {})", path, keys.asScala, sec.ttl()) + sec } + private def filter( + configData: ValueWithTtl[Map[String, String]], + keys: Set[String], + ): ValueWithTtl[Map[String, String]] = + configData.copy(value = configData.value.filter { case (k, _) => keys.contains(k) }) + override def close(): Unit = tokenRenewal.foreach(_.close()) // get the secrets and ttl under a path - def getSecrets( - path: String, - ): Map[String, (String, Option[OffsetDateTime])] = { - val now = OffsetDateTime.now() - - logger.info(s"Looking up value at [$path]") - val fileWriter = new FileWriterOnce(Paths.get(settings.fileDir, path)) + val getSecretValue: String => Either[Throwable, ValueWithTtl[Map[String, String]]] = path => { + logger.debug(s"Looking up value from Vault at [$path]") Try(vaultClient.get.logical().read(path)) match { + case Failure(ex) => + failWithEx(s"Failed to fetch secrets from path [$path]", ex) + case Success(response) if response.getRestResponse.getStatus != 200 => + failWithEx( + s"No secrets found at path [$path]. Vault response: ${new String(response.getRestResponse.getBody)}", + ) + case Success(response) if response.getData.isEmpty => + failWithEx(s"No secrets found at path [$path]") case Success(response) => - if (response.getRestResponse.getStatus != 200) { - throw new ConnectException( - s"No secrets found at path [$path]. Vault response: ${new String(response.getRestResponse.getBody)}", - ) - } - - val ttl = Option(vaultClient.get.logical().read(path).getLeaseDuration) match { - case Some(duration) => Some(now.plusSeconds(duration)) - case None => None - } - - if (response.getData.isEmpty) { - throw new ConnectException( - s"No secrets found at path [$path]", - ) - } - - response.getData.asScala.map { - case (k, v) => - val encodingAndId = EncodingAndId.from(k) - val decoded = - decodeKey( - encoding = encodingAndId.encoding, - key = k, - value = v, - writeFileFn = { content => - fileWriter.write(k.toLowerCase, content, k).toString - }, - ) - - (k, (decoded, ttl)) - }.toMap - - case Failure(exception) => - throw new ConnectException( - s"Failed to fetch secrets from path [$path]", - exception, + val ttl = + Option(response.getLeaseDuration).filterNot(_ == 0L).map(Duration(_, TimeUnit.SECONDS)) + Right( + ValueWithTtl(ttl, settings.defaultTtl, parseSuccessfulResponse(path, response)), ) } } + + private def parseSuccessfulResponse( + path: String, + response: LogicalResponse, + ) = { + val fileWriterMaybe = settings.fileWriterOpts.map(_.createFileWriter(path)) + response.getData.asScala.map { + case (k, v) => + val encodingAndId = EncodingAndId.from(k) + val decoded = + decodeKey( + encoding = encodingAndId.encoding, + key = k, + value = v, + writeFileFn = { content => + fileWriterMaybe.fold("nofile")(_.write(k.toLowerCase, content, k).toString) + }, + ) + (k, decoded) + }.toMap + } + } diff --git a/secret-provider/src/main/scala/io/lenses/connect/secrets/utils/ConfigDataBuilder.scala b/secret-provider/src/main/scala/io/lenses/connect/secrets/utils/ConfigDataBuilder.scala new file mode 100644 index 0000000..c85ebfc --- /dev/null +++ b/secret-provider/src/main/scala/io/lenses/connect/secrets/utils/ConfigDataBuilder.scala @@ -0,0 +1,17 @@ +package io.lenses.connect.secrets.utils + +import io.lenses.connect.secrets.cache.ValueWithTtl +import org.apache.kafka.common.config.ConfigData + +import java.time.Clock +import scala.jdk.CollectionConverters.MapHasAsJava + +object ConfigDataBuilder { + + def apply(valueWithTtl: ValueWithTtl[Map[String, String]])(implicit clock: Clock) = + new ConfigData( + valueWithTtl.value.asJava, + valueWithTtl.ttlAndExpiry.map(t => Long.box(t.ttlRemaining.toMillis)).orNull, + ) + +} diff --git a/secret-provider/src/main/scala/io/lenses/connect/secrets/utils/ExceptionUtils.scala b/secret-provider/src/main/scala/io/lenses/connect/secrets/utils/ExceptionUtils.scala new file mode 100644 index 0000000..5cad226 --- /dev/null +++ b/secret-provider/src/main/scala/io/lenses/connect/secrets/utils/ExceptionUtils.scala @@ -0,0 +1,17 @@ +package io.lenses.connect.secrets.utils + +import org.apache.kafka.connect.errors.ConnectException + +object ExceptionUtils { + + def failWithEx[T](error: String, ex: Throwable): Either[Throwable, T] = + failWithEx(error, Some(ex)) + + def failWithEx[T]( + error: String, + ex: Option[Throwable] = Option.empty, + ): Either[Throwable, T] = + Left( + new ConnectException(error, ex.orNull), + ) +} diff --git a/secret-provider/src/test/scala/io/lenses/connect/secrets/cache/TtlTest.scala b/secret-provider/src/test/scala/io/lenses/connect/secrets/cache/TtlTest.scala new file mode 100644 index 0000000..2c8f408 --- /dev/null +++ b/secret-provider/src/test/scala/io/lenses/connect/secrets/cache/TtlTest.scala @@ -0,0 +1,42 @@ +package io.lenses.connect.secrets.cache + +import org.scalatest.OptionValues +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +import java.time.temporal.ChronoUnit +import java.time.Clock +import java.time.Instant +import java.time.ZoneId +import scala.concurrent.duration.Duration +import scala.concurrent.duration.MINUTES + +class TtlTest extends AnyFunSuite with Matchers with OptionValues { + implicit val clock = Clock.fixed(Instant.EPOCH, ZoneId.systemDefault()) + + test("no durations should lead to empty TTL") { + Ttl(Option.empty, Option.empty) should be(Option.empty) + } + + test("ttl returned with record should be used") { + val ttl = Ttl(Some(Duration(5, MINUTES)), Option.empty) + ttl.value.originalTtl should be(Duration(5, MINUTES)) + ttl.value.expiry should be(Instant.EPOCH.plus(5, ChronoUnit.MINUTES).atOffset(toOffset)) + } + + private def toOffset = + clock.getZone.getRules.getOffset(clock.instant()) + + test("default ttl should apply if not available") { + val ttl = Ttl(Option.empty, Some(Duration(5, MINUTES))) + ttl.value.originalTtl should be(Duration(5, MINUTES)) + ttl.value.expiry should be(Instant.EPOCH.plus(5, ChronoUnit.MINUTES).atOffset(toOffset)) + } + + test("record ttl should be preferred over default") { + val ttl = Ttl(Some(Duration(10, MINUTES)), Some(Duration(5, MINUTES))) + ttl.value.originalTtl should be(Duration(10, MINUTES)) + ttl.value.expiry should be(Instant.EPOCH.plus(10, ChronoUnit.MINUTES).atOffset(toOffset)) + } + +} diff --git a/secret-provider/src/test/scala/io/lenses/connect/secrets/providers/VaultSecretProviderTest.scala b/secret-provider/src/test/scala/io/lenses/connect/secrets/providers/VaultSecretProviderTest.scala index 1e440d3..5165ef0 100644 --- a/secret-provider/src/test/scala/io/lenses/connect/secrets/providers/VaultSecretProviderTest.scala +++ b/secret-provider/src/test/scala/io/lenses/connect/secrets/providers/VaultSecretProviderTest.scala @@ -58,6 +58,8 @@ class VaultSecretProviderTest extends AnyWordSpec with Matchers with BeforeAndAf val root: JsonObject = new JsonObject() .add("data", data) .add("auth", auth) + .add("lease_id", "abcdefg") + .add("lease_duration", 10000L) .add("renewable", true) val mockVault = new MockVault(200, root.toString) val server: Server = VaultTestUtils.initHttpsMockVault(mockVault) @@ -103,6 +105,35 @@ class VaultSecretProviderTest extends AnyWordSpec with Matchers with BeforeAndAf provider.tokenRenewalSuccess >= 4 shouldBe true } + "should recalculate secret ttl when cached" in { + + // set up secret + val props = Map( + VaultProviderConfig.VAULT_ADDR -> "https://127.0.0.1:9998", + VaultProviderConfig.VAULT_TOKEN -> "mock_token", + VaultProviderConfig.VAULT_PEM -> pemFile, + VaultProviderConfig.VAULT_CLIENT_PEM -> pemFile, + connect.FILE_DIR -> tmp, + ).asJava + + val config = VaultProviderConfig(props) + val settings = VaultSettings(config) + + settings.pem shouldBe pemFile + val provider = new VaultSecretProvider() + provider.configure(props) + + val data1 = provider.get("secret/hello") + + Thread.sleep(5000) + + val data2 = provider.get("secret/hello") + + (data1.ttl > data2.ttl) shouldBe true + (data1.ttl() - data2.ttl()) should be > 5000L + data2.ttl().longValue() should be < 9995000L + } + "should be configured for username and password auth" in { val props = Map( @@ -317,6 +348,7 @@ class VaultSecretProviderTest extends AnyWordSpec with Matchers with BeforeAndAf VaultProviderConfig.VAULT_PEM -> pemFile, VaultProviderConfig.VAULT_CLIENT_PEM -> pemFile, connect.FILE_DIR -> tmp, + connect.WRITE_FILES -> true, ).asJava val provider = new VaultSecretProvider() @@ -347,6 +379,7 @@ class VaultSecretProviderTest extends AnyWordSpec with Matchers with BeforeAndAf VaultProviderConfig.VAULT_PEM -> pemFile, VaultProviderConfig.VAULT_CLIENT_PEM -> pemFile, connect.FILE_DIR -> tmp, + connect.WRITE_FILES -> true, ).asJava val provider = new VaultSecretProvider() diff --git a/secret-provider/src/test/scala/io/lenses/connect/secrets/utils/ConfigDataBuilderTest.scala b/secret-provider/src/test/scala/io/lenses/connect/secrets/utils/ConfigDataBuilderTest.scala new file mode 100644 index 0000000..301b2c2 --- /dev/null +++ b/secret-provider/src/test/scala/io/lenses/connect/secrets/utils/ConfigDataBuilderTest.scala @@ -0,0 +1,27 @@ +package io.lenses.connect.secrets.utils + +import io.lenses.connect.secrets.cache.ValueWithTtl +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +import java.time.Clock +import scala.concurrent.duration.Duration +import scala.concurrent.duration.MINUTES + +class ConfigDataBuilderTest extends AnyFunSuite with Matchers { + + implicit val clock = Clock.systemDefaultZone() + + test("Converts to the expected java structure") { + val map = ValueWithTtl( + Option(Duration(1, MINUTES)), + Option.empty[Duration], + Map[String, String]( + "secret" -> "12345", + ), + ) + val ret = ConfigDataBuilder(map) + ret.ttl().longValue() should be > 0L + ret.data().get("secret") should be("12345") + } +} diff --git a/test-sink/src/main/scala/io/lenses/connect/secrets/test/TestSinkConnector.scala b/test-sink/src/main/scala/io/lenses/connect/secrets/test/TestSinkConnector.scala new file mode 100644 index 0000000..ceffc3f --- /dev/null +++ b/test-sink/src/main/scala/io/lenses/connect/secrets/test/TestSinkConnector.scala @@ -0,0 +1,45 @@ +package io.lenses.connect.secrets.test + +import com.typesafe.scalalogging.LazyLogging +import org.apache.kafka.common.config.ConfigDef +import org.apache.kafka.common.config.ConfigDef.Importance +import org.apache.kafka.connect.connector.Task +import org.apache.kafka.connect.sink.SinkConnector + +import java.util +import scala.jdk.CollectionConverters.MapHasAsScala +import scala.jdk.CollectionConverters.SeqHasAsJava + +class TestSinkConnector extends SinkConnector with LazyLogging { + + private val storedProps: util.Map[String, String] = + new util.HashMap[String, String]() + + override def start(props: util.Map[String, String]): Unit = { + logger.info("start(props:{})", props.asScala) + storedProps.putAll(props) + } + + override def taskClass(): Class[_ <: Task] = classOf[TestSinkTask] + + override def taskConfigs( + maxTasks: Int, + ): util.List[util.Map[String, String]] = { + logger.info("testConfigs(maxTasks:{})", maxTasks) + List.fill(1)(storedProps).asJava + } + + override def stop(): Unit = { + logger.info("stop()") + () + } + + override def config(): ConfigDef = new ConfigDef() + .define("test.sink.vault.host", ConfigDef.Type.STRING, Importance.LOW, "host") + .define("test.sink.vault.token", ConfigDef.Type.STRING, Importance.LOW, "token") + .define("test.sink.secret.value", ConfigDef.Type.STRING, Importance.LOW, "value") + .define("test.sink.secret.path", ConfigDef.Type.STRING, Importance.LOW, "path") + .define("test.sink.secret.key", ConfigDef.Type.PASSWORD, Importance.LOW, "key") + + override def version(): String = "0.0.1a" +} diff --git a/test-sink/src/main/scala/io/lenses/connect/secrets/test/TestSinkTask.scala b/test-sink/src/main/scala/io/lenses/connect/secrets/test/TestSinkTask.scala new file mode 100644 index 0000000..2271abe --- /dev/null +++ b/test-sink/src/main/scala/io/lenses/connect/secrets/test/TestSinkTask.scala @@ -0,0 +1,38 @@ +package io.lenses.connect.secrets.test + +import com.typesafe.scalalogging.LazyLogging +import io.lenses.connect.secrets.test.VaultStateValidator.validateSecret +import org.apache.kafka.connect.sink.SinkRecord +import org.apache.kafka.connect.sink.SinkTask +import org.apache.kafka.connect.sink.SinkTaskContext + +import java.util +import scala.jdk.CollectionConverters.CollectionHasAsScala +import scala.jdk.CollectionConverters.MapHasAsScala + +class TestSinkTask extends SinkTask with LazyLogging { + + private implicit var vaultState: VaultState = _ + + override def start(props: util.Map[String, String]): Unit = { + val scalaProps = props.asScala.toMap + logger.info("start(props:{})", scalaProps) + vaultState = VaultState(scalaProps) + logger.info("start(vaultState:{})", vaultState) + } + + override def put(records: util.Collection[SinkRecord]): Unit = { + logger.info("put(records:{})", records.asScala) + validateSecret() + } + + override def stop(): Unit = + logger.info("stop()") + + override def initialize(context: SinkTaskContext): Unit = { + super.initialize(context) + logger.info("initialize(configs:{})", context.configs().asScala) + } + + override def version(): String = "0.0.1a" +} diff --git a/test-sink/src/main/scala/io/lenses/connect/secrets/test/VaultState.scala b/test-sink/src/main/scala/io/lenses/connect/secrets/test/VaultState.scala new file mode 100644 index 0000000..ab71c49 --- /dev/null +++ b/test-sink/src/main/scala/io/lenses/connect/secrets/test/VaultState.scala @@ -0,0 +1,48 @@ +package io.lenses.connect.secrets.test + +import com.bettercloud.vault.Vault +import com.bettercloud.vault.VaultConfig + +case class VaultState( + vault: Vault, + secretValue: String, + secretPath: String, + secretKey: String, +) + +object VaultState { + def apply(props: Map[String, String]): VaultState = { + + val vaultHost = props.getOrElse( + "test.sink.vault.host", + throw new IllegalStateException("No test.sink.vault.host"), + ) + val vaultToken = props.getOrElse( + "test.sink.vault.token", + throw new IllegalStateException("No test.sink.vault.token"), + ) + val secret = props.getOrElse( + "test.sink.secret.value", + throw new IllegalStateException("No test.sink.secret.value"), + ) + val secretPath = props.getOrElse( + "test.sink.secret.path", + throw new IllegalStateException("No test.sink.secret.path"), + ) + val secretKey = props.getOrElse( + "test.sink.secret.key", + throw new IllegalStateException("No test.sink.secret.key"), + ) + VaultState( + new Vault( + new VaultConfig() + .address(vaultHost) + .token(vaultToken) + .build(), + ), + secret, + secretPath, + secretKey, + ) + } +} diff --git a/test-sink/src/main/scala/io/lenses/connect/secrets/test/VaultStateValidator.scala b/test-sink/src/main/scala/io/lenses/connect/secrets/test/VaultStateValidator.scala new file mode 100644 index 0000000..dc6fe7c --- /dev/null +++ b/test-sink/src/main/scala/io/lenses/connect/secrets/test/VaultStateValidator.scala @@ -0,0 +1,59 @@ +package io.lenses.connect.secrets.test + +import com.typesafe.scalalogging.LazyLogging + +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import scala.collection.mutable +import scala.jdk.CollectionConverters.MapHasAsScala + +object VaultStateValidator extends LazyLogging { + + private case class VaultSecret(created: LocalDateTime, expiry: LocalDateTime, secretValue: String) + + def validateSecret()(implicit vaultState: VaultState) = { + + val vaultSecret = getSecretFromVault() + + val nowInst = LocalDateTime.now() + + logger.info( + "Secret info (created: {}, expires: {}, now: {})", + vaultSecret.created, + vaultSecret.expiry, + nowInst, + ) + + require(nowInst.isAfter(vaultSecret.created), "secret isn't active yet") + require(nowInst.isBefore(vaultSecret.expiry), "secret has expired") + + } + + private def getSecretFromVault()(implicit vaultState: VaultState): VaultSecret = { + val vaultSecret = vaultState.vault.logical().read(vaultState.secretPath) + val vaultData = vaultSecret.getData.asScala + val secretValue = vaultData.getOrElse( + vaultState.secretKey, + throw new IllegalStateException("Secret has no value"), + ) + VaultSecret( + extractDateFromVaultData(vaultData, "created"), + extractDateFromVaultData(vaultData, "expires"), + secretValue, + ) + } + + private def extractDateFromVaultData(vaultData: mutable.Map[String, String], fieldName: String) = { + val timeMillis = vaultData + .get(fieldName) + .map(java.lang.Long.valueOf) + .getOrElse(throw new IllegalStateException(s"Secret has no $fieldName time")) + + Instant + .ofEpochMilli(timeMillis) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime + } + +}