From 8927fdbaeeef011ef803264c7775b63df91202c7 Mon Sep 17 00:00:00 2001 From: Mykhailo Hodovaniuk Date: Mon, 6 Jun 2022 21:47:59 +0300 Subject: [PATCH 1/6] Support login with role_id and secret_id --- .../main/scala/com/banno/vault/Vault.scala | 15 ++++--- .../scala/com/banno/vault/VaultSpec.scala | 45 ++++++++++++------- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/core/src/main/scala/com/banno/vault/Vault.scala b/core/src/main/scala/com/banno/vault/Vault.scala index 4654dbf5..991068e9 100644 --- a/core/src/main/scala/com/banno/vault/Vault.scala +++ b/core/src/main/scala/com/banno/vault/Vault.scala @@ -37,15 +37,20 @@ object Vault { /** * https://www.vaultproject.io/api/auth/approle/index.html#login-with-approle */ - def login[F[_]](client: Client[F], vaultUri: Uri)(roleId: String)(implicit F: Concurrent[F]): F[VaultToken] = { + def login[F[_]](client: Client[F], vaultUri: Uri)(roleId: String, secretId: Option[String] = None)(implicit F: Concurrent[F]): F[VaultToken] = { val request = Request[F]( method = Method.POST, uri = vaultUri / "v1" / "auth" / "approle" / "login" - ).withEntity(Json.obj(("role_id", Json.fromString(roleId)))) + ).withEntity( + Json.fromFields( + Seq("role_id" -> Json.fromString(roleId)) ++ + secretId.fold(Seq())(sId => Seq("secret_id" -> Json.fromString(sId))) + ) + ) for { json <- F.handleErrorWith(client.expect[Json](request) ) { e => - F.raiseError(VaultRequestError(request, e.some, s"roleId=$roleId".some)) + F.raiseError(VaultRequestError(request, e.some, s"roleId=$roleId, secretId=$secretId".some)) } token <- raiseKnownError(json.hcursor.get[VaultToken]("auth"))(decoderError) } yield token @@ -228,8 +233,8 @@ object Vault { } def loginAndKeepSecretLeased[F[_]: Temporal, A: Decoder](client: Client[F], vaultUri: Uri) - (roleId: String, secretPath: String, duration: FiniteDuration, waitInterval: FiniteDuration): Stream[F, A] = - Stream.eval(login(client, vaultUri)(roleId)).flatMap(token => keepLoginAndSecretLeased[F, A](client, vaultUri)(token, secretPath, duration, waitInterval)) + (roleId: String, secretId: Option[String], secretPath: String, duration: FiniteDuration, waitInterval: FiniteDuration): Stream[F, A] = + Stream.eval(login(client, vaultUri)(roleId, secretId)).flatMap(token => keepLoginAndSecretLeased[F, A](client, vaultUri)(token, secretPath, duration, waitInterval)) def loginK8sAndKeepSecretLeased[F[_]: Temporal, A: Decoder](client: Client[F], vaultUri: Uri) (roleId: String, jwt: String, secretPath: String, duration: FiniteDuration, waitInterval: FiniteDuration, loginMountPoint: Uri.Path = path"/auth/kubernetes" ): Stream[F, A] = diff --git a/core/src/test/scala/com/banno/vault/VaultSpec.scala b/core/src/test/scala/com/banno/vault/VaultSpec.scala index 141b518a..ae329d2c 100644 --- a/core/src/test/scala/com/banno/vault/VaultSpec.scala +++ b/core/src/test/scala/com/banno/vault/VaultSpec.scala @@ -39,11 +39,9 @@ import org.typelevel.ci.CIString class VaultSpec extends CatsEffectSuite with ScalaCheckEffectSuite with MissingPieces { - case class RoleId(role_id: String) - object RoleId { - implicit val roleIdDecoder: Decoder[RoleId] = Decoder.instance[RoleId] { c => - Decoder.resultInstance.map(c.downField("role_id").as[String])(RoleId(_)) - } + case class RoleIdAndSecretId(roleId: String, secretId: Option[String]) + object RoleIdAndSecretId { + implicit val roleIdAndSecretIdDecoder: Decoder[RoleIdAndSecretId] = Decoder.forProduct2("role_id", "secret_id")(RoleIdAndSecretId.apply) } case class RoleAndJwt(role: String, jwt: String) @@ -93,10 +91,11 @@ class VaultSpec extends CatsEffectSuite with ScalaCheckEffectSuite with MissingP val private_key_type: String = UUID.randomUUID().toString val serial_number: String = UUID.randomUUID().toString - val validRoleId: String = UUID.randomUUID().toString - val invalidJSONRoleId: String = UUID.randomUUID().toString - val roleIdWithoutToken: String = UUID.randomUUID().toString - val roleIdWithoutLease: String = UUID.randomUUID().toString + val validRoleId: String = UUID.randomUUID().toString + val validRoleIdAndSecretId: (String, Option[String]) = UUID.randomUUID().toString -> Some(UUID.randomUUID().toString) + val invalidJSONRoleId: String = UUID.randomUUID().toString + val roleIdWithoutToken: String = UUID.randomUUID().toString + val roleIdWithoutLease: String = UUID.randomUUID().toString val validKubernetesRole: String = UUID.randomUUID().toString val validKubernetesJwt: String = Random.alphanumeric.take(20).mkString //simulate a signed jwt https://www.vaultproject.io/api/auth/kubernetes/index.html#login @@ -159,8 +158,17 @@ class VaultSpec extends CatsEffectSuite with ScalaCheckEffectSuite with MissingP checkVaultToken(req)( NoContent() ) case req @ POST -> Root / "v1" / "auth" / "approle" / "login" => - req.decodeJson[RoleId].flatMap { - case RoleId(`validRoleId`) => + req.decodeJson[RoleIdAndSecretId].flatMap { + case RoleIdAndSecretId(`validRoleId`, `None`) => + Ok(s""" + |{ + | "auth": { + | "client_token": "$clientToken", + | "lease_duration": $leaseDuration, + | "renewable": $renewable + | } + |}""".stripMargin) + case roleIdAndSecretId if roleIdAndSecretId.roleId == validRoleIdAndSecretId._1 && roleIdAndSecretId.secretId == validRoleIdAndSecretId._2 => Ok(s""" |{ | "auth": { @@ -169,16 +177,16 @@ class VaultSpec extends CatsEffectSuite with ScalaCheckEffectSuite with MissingP | "renewable": $renewable | } |}""".stripMargin) - case RoleId(`invalidJSONRoleId`) => + case RoleIdAndSecretId(`invalidJSONRoleId`, `None`) => Ok(s""" NOT A JSON """) - case RoleId(`roleIdWithoutToken`) => + case RoleIdAndSecretId(`roleIdWithoutToken`, `None`) => Ok(s""" |{ | "auth": { | "lease_duration": $leaseDuration | } |}""".stripMargin) - case RoleId(`roleIdWithoutLease`) => + case RoleIdAndSecretId(`roleIdWithoutLease`, `None`) => Ok(s""" |{ | "auth": { @@ -312,6 +320,12 @@ class VaultSpec extends CatsEffectSuite with ScalaCheckEffectSuite with MissingP } } + test("login works as expected when sending a valid roleId and secretId") { + PropF.forAllF(VaultArbitraries.validVaultUri) { uri => + Vault.login(mockClient, uri)(validRoleIdAndSecretId._1, validRoleIdAndSecretId._2).assertEquals(validToken) + } + } + test("login should fail when sending an invalid roleId") { PropF.forAllF(VaultArbitraries.validVaultUri){uri => Vault.login(mockClient, uri)(UUID.randomUUID().toString) @@ -490,13 +504,14 @@ class VaultSpec extends CatsEffectSuite with ScalaCheckEffectSuite with MissingP Arbitrary.arbitrary[FiniteDuration] ) { case (uri, leaseDuration, waitInterval) => PropF.boolean[IO](leaseDuration < waitInterval) ==> { - Vault.loginAndKeepSecretLeased[IO, Unit](mockClient, uri)(validRoleId, "", leaseDuration, waitInterval) + Vault.loginAndKeepSecretLeased[IO, Unit](mockClient, uri)(validRoleId, None, "", leaseDuration, waitInterval) .attempt .compile .last .assertEquals(Some(Left(Vault.InvalidRequirement("waitInterval longer than requested Lease Duration")))) }} } + test("loginK8sAndKeepSecretLeased fails when wait duration is longer than lease duration") { PropF.forAllF( VaultArbitraries.validVaultUri, From 4bcb82b3b76ac4b52371d88a83f7bb19c6e8dfab Mon Sep 17 00:00:00 2001 From: Mykhailo Hodovaniuk Date: Mon, 6 Jun 2022 23:02:48 +0300 Subject: [PATCH 2/6] Fix build --- core/src/main/scala/com/banno/vault/Vault.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/com/banno/vault/Vault.scala b/core/src/main/scala/com/banno/vault/Vault.scala index 991068e9..0149a9a2 100644 --- a/core/src/main/scala/com/banno/vault/Vault.scala +++ b/core/src/main/scala/com/banno/vault/Vault.scala @@ -44,7 +44,7 @@ object Vault { ).withEntity( Json.fromFields( Seq("role_id" -> Json.fromString(roleId)) ++ - secretId.fold(Seq())(sId => Seq("secret_id" -> Json.fromString(sId))) + secretId.fold(Seq[(String, Json)]())(sId => Seq("secret_id" -> Json.fromString(sId))) ) ) for { From 2fadd153efdc54d15352da6dcf20ef67dd578da0 Mon Sep 17 00:00:00 2001 From: Mykhailo Hodovaniuk Date: Tue, 7 Jun 2022 11:10:20 +0300 Subject: [PATCH 3/6] Fix build --- build.sbt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.sbt b/build.sbt index 3042f84a..b515d8bb 100644 --- a/build.sbt +++ b/build.sbt @@ -85,8 +85,11 @@ lazy val core = project.in(file("core")) mimaBinaryIssueFilters ++= { import com.typesafe.tools.mima.core.IncompatibleSignatureProblem import com.typesafe.tools.mima.core.ProblemFilters.exclude + import com.typesafe.tools.mima.core.DirectMissingMethodProblem // See https://github.com/lightbend/mima/issues/423 Seq( + exclude[DirectMissingMethodProblem]("com.banno.vault.Vault.login"), + exclude[DirectMissingMethodProblem]("com.banno.vault.Vault.loginAndKeepSecretLeased"), ) }, ) From 30aac325adb30bbba7f8e72f5e58561d7abfcc09 Mon Sep 17 00:00:00 2001 From: Mykhailo Hodovaniuk Date: Tue, 7 Jun 2022 12:04:08 +0300 Subject: [PATCH 4/6] Rename secretId to roleSecretId --- .../main/scala/com/banno/vault/Vault.scala | 6 ++-- .../scala/com/banno/vault/VaultSpec.scala | 32 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/core/src/main/scala/com/banno/vault/Vault.scala b/core/src/main/scala/com/banno/vault/Vault.scala index 0149a9a2..8e2dc590 100644 --- a/core/src/main/scala/com/banno/vault/Vault.scala +++ b/core/src/main/scala/com/banno/vault/Vault.scala @@ -37,20 +37,20 @@ object Vault { /** * https://www.vaultproject.io/api/auth/approle/index.html#login-with-approle */ - def login[F[_]](client: Client[F], vaultUri: Uri)(roleId: String, secretId: Option[String] = None)(implicit F: Concurrent[F]): F[VaultToken] = { + def login[F[_]](client: Client[F], vaultUri: Uri)(roleId: String, roleSecretId: Option[String] = None)(implicit F: Concurrent[F]): F[VaultToken] = { val request = Request[F]( method = Method.POST, uri = vaultUri / "v1" / "auth" / "approle" / "login" ).withEntity( Json.fromFields( Seq("role_id" -> Json.fromString(roleId)) ++ - secretId.fold(Seq[(String, Json)]())(sId => Seq("secret_id" -> Json.fromString(sId))) + roleSecretId.fold(Seq[(String, Json)]())(sId => Seq("secret_id" -> Json.fromString(sId))) ) ) for { json <- F.handleErrorWith(client.expect[Json](request) ) { e => - F.raiseError(VaultRequestError(request, e.some, s"roleId=$roleId, secretId=$secretId".some)) + F.raiseError(VaultRequestError(request, e.some, s"roleId=$roleId, roleSecretId=$roleSecretId".some)) } token <- raiseKnownError(json.hcursor.get[VaultToken]("auth"))(decoderError) } yield token diff --git a/core/src/test/scala/com/banno/vault/VaultSpec.scala b/core/src/test/scala/com/banno/vault/VaultSpec.scala index ae329d2c..439bf9eb 100644 --- a/core/src/test/scala/com/banno/vault/VaultSpec.scala +++ b/core/src/test/scala/com/banno/vault/VaultSpec.scala @@ -39,9 +39,9 @@ import org.typelevel.ci.CIString class VaultSpec extends CatsEffectSuite with ScalaCheckEffectSuite with MissingPieces { - case class RoleIdAndSecretId(roleId: String, secretId: Option[String]) - object RoleIdAndSecretId { - implicit val roleIdAndSecretIdDecoder: Decoder[RoleIdAndSecretId] = Decoder.forProduct2("role_id", "secret_id")(RoleIdAndSecretId.apply) + case class RoleIdAndRoleSecretId(roleId: String, roleSecretId: Option[String]) + object RoleIdAndRoleSecretId { + implicit val roleIdAndSecretIdDecoder: Decoder[RoleIdAndRoleSecretId] = Decoder.forProduct2("role_id", "secret_id")(RoleIdAndRoleSecretId.apply) } case class RoleAndJwt(role: String, jwt: String) @@ -91,11 +91,11 @@ class VaultSpec extends CatsEffectSuite with ScalaCheckEffectSuite with MissingP val private_key_type: String = UUID.randomUUID().toString val serial_number: String = UUID.randomUUID().toString - val validRoleId: String = UUID.randomUUID().toString - val validRoleIdAndSecretId: (String, Option[String]) = UUID.randomUUID().toString -> Some(UUID.randomUUID().toString) - val invalidJSONRoleId: String = UUID.randomUUID().toString - val roleIdWithoutToken: String = UUID.randomUUID().toString - val roleIdWithoutLease: String = UUID.randomUUID().toString + val validRoleId: String = UUID.randomUUID().toString + val validRoleIdAndRoleSecretId: (String, Option[String]) = UUID.randomUUID().toString -> Some(UUID.randomUUID().toString) + val invalidJSONRoleId: String = UUID.randomUUID().toString + val roleIdWithoutToken: String = UUID.randomUUID().toString + val roleIdWithoutLease: String = UUID.randomUUID().toString val validKubernetesRole: String = UUID.randomUUID().toString val validKubernetesJwt: String = Random.alphanumeric.take(20).mkString //simulate a signed jwt https://www.vaultproject.io/api/auth/kubernetes/index.html#login @@ -158,8 +158,8 @@ class VaultSpec extends CatsEffectSuite with ScalaCheckEffectSuite with MissingP checkVaultToken(req)( NoContent() ) case req @ POST -> Root / "v1" / "auth" / "approle" / "login" => - req.decodeJson[RoleIdAndSecretId].flatMap { - case RoleIdAndSecretId(`validRoleId`, `None`) => + req.decodeJson[RoleIdAndRoleSecretId].flatMap { + case RoleIdAndRoleSecretId(`validRoleId`, `None`) => Ok(s""" |{ | "auth": { @@ -168,7 +168,7 @@ class VaultSpec extends CatsEffectSuite with ScalaCheckEffectSuite with MissingP | "renewable": $renewable | } |}""".stripMargin) - case roleIdAndSecretId if roleIdAndSecretId.roleId == validRoleIdAndSecretId._1 && roleIdAndSecretId.secretId == validRoleIdAndSecretId._2 => + case roleIdAndRoleSecretId if roleIdAndRoleSecretId.roleId == validRoleIdAndRoleSecretId._1 && roleIdAndRoleSecretId.roleSecretId == validRoleIdAndRoleSecretId._2 => Ok(s""" |{ | "auth": { @@ -177,16 +177,16 @@ class VaultSpec extends CatsEffectSuite with ScalaCheckEffectSuite with MissingP | "renewable": $renewable | } |}""".stripMargin) - case RoleIdAndSecretId(`invalidJSONRoleId`, `None`) => + case RoleIdAndRoleSecretId(`invalidJSONRoleId`, `None`) => Ok(s""" NOT A JSON """) - case RoleIdAndSecretId(`roleIdWithoutToken`, `None`) => + case RoleIdAndRoleSecretId(`roleIdWithoutToken`, `None`) => Ok(s""" |{ | "auth": { | "lease_duration": $leaseDuration | } |}""".stripMargin) - case RoleIdAndSecretId(`roleIdWithoutLease`, `None`) => + case RoleIdAndRoleSecretId(`roleIdWithoutLease`, `None`) => Ok(s""" |{ | "auth": { @@ -320,9 +320,9 @@ class VaultSpec extends CatsEffectSuite with ScalaCheckEffectSuite with MissingP } } - test("login works as expected when sending a valid roleId and secretId") { + test("login works as expected when sending a valid roleId and roleSecretId") { PropF.forAllF(VaultArbitraries.validVaultUri) { uri => - Vault.login(mockClient, uri)(validRoleIdAndSecretId._1, validRoleIdAndSecretId._2).assertEquals(validToken) + Vault.login(mockClient, uri)(validRoleIdAndRoleSecretId._1, validRoleIdAndRoleSecretId._2).assertEquals(validToken) } } From 0348bc66869341a4db443c98046e4e2410a29f26 Mon Sep 17 00:00:00 2001 From: Mykhailo Hodovaniuk Date: Tue, 7 Jun 2022 12:04:08 +0300 Subject: [PATCH 5/6] Rename secretId to roleSecretId --- core/src/main/scala/com/banno/vault/Vault.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/com/banno/vault/Vault.scala b/core/src/main/scala/com/banno/vault/Vault.scala index 8e2dc590..57e1eb26 100644 --- a/core/src/main/scala/com/banno/vault/Vault.scala +++ b/core/src/main/scala/com/banno/vault/Vault.scala @@ -233,8 +233,8 @@ object Vault { } def loginAndKeepSecretLeased[F[_]: Temporal, A: Decoder](client: Client[F], vaultUri: Uri) - (roleId: String, secretId: Option[String], secretPath: String, duration: FiniteDuration, waitInterval: FiniteDuration): Stream[F, A] = - Stream.eval(login(client, vaultUri)(roleId, secretId)).flatMap(token => keepLoginAndSecretLeased[F, A](client, vaultUri)(token, secretPath, duration, waitInterval)) + (roleId: String, roleSecretId: Option[String], secretPath: String, duration: FiniteDuration, waitInterval: FiniteDuration): Stream[F, A] = + Stream.eval(login(client, vaultUri)(roleId, roleSecretId)).flatMap(token => keepLoginAndSecretLeased[F, A](client, vaultUri)(token, secretPath, duration, waitInterval)) def loginK8sAndKeepSecretLeased[F[_]: Temporal, A: Decoder](client: Client[F], vaultUri: Uri) (roleId: String, jwt: String, secretPath: String, duration: FiniteDuration, waitInterval: FiniteDuration, loginMountPoint: Uri.Path = path"/auth/kubernetes" ): Stream[F, A] = From 20f46cc9b0ff443eb4be53466b45cc98f119c05f Mon Sep 17 00:00:00 2001 From: Mykhailo Hodovaniuk Date: Thu, 9 Jun 2022 12:51:00 +0300 Subject: [PATCH 6/6] Pass roleSecretId to login with builder pattern --- build.sbt | 1 + .../main/scala/com/banno/vault/Vault.scala | 85 ++++++++++++++----- .../scala/com/banno/vault/VaultSpec.scala | 31 +++++-- 3 files changed, 86 insertions(+), 31 deletions(-) diff --git a/build.sbt b/build.sbt index b515d8bb..f8bf1a53 100644 --- a/build.sbt +++ b/build.sbt @@ -89,6 +89,7 @@ lazy val core = project.in(file("core")) // See https://github.com/lightbend/mima/issues/423 Seq( exclude[DirectMissingMethodProblem]("com.banno.vault.Vault.login"), + exclude[DirectMissingMethodProblem]("com.banno.vault.Vault.loginAndKeep"), exclude[DirectMissingMethodProblem]("com.banno.vault.Vault.loginAndKeepSecretLeased"), ) }, diff --git a/core/src/main/scala/com/banno/vault/Vault.scala b/core/src/main/scala/com/banno/vault/Vault.scala index 57e1eb26..bdebbf01 100644 --- a/core/src/main/scala/com/banno/vault/Vault.scala +++ b/core/src/main/scala/com/banno/vault/Vault.scala @@ -37,23 +37,8 @@ object Vault { /** * https://www.vaultproject.io/api/auth/approle/index.html#login-with-approle */ - def login[F[_]](client: Client[F], vaultUri: Uri)(roleId: String, roleSecretId: Option[String] = None)(implicit F: Concurrent[F]): F[VaultToken] = { - val request = Request[F]( - method = Method.POST, - uri = vaultUri / "v1" / "auth" / "approle" / "login" - ).withEntity( - Json.fromFields( - Seq("role_id" -> Json.fromString(roleId)) ++ - roleSecretId.fold(Seq[(String, Json)]())(sId => Seq("secret_id" -> Json.fromString(sId))) - ) - ) - for { - json <- F.handleErrorWith(client.expect[Json](request) - ) { e => - F.raiseError(VaultRequestError(request, e.some, s"roleId=$roleId, roleSecretId=$roleSecretId".some)) - } - token <- raiseKnownError(json.hcursor.get[VaultToken]("auth"))(decoderError) - } yield token + def login[F[_]](client: Client[F], vaultUri: Uri): VaultLoginOperation[F] = { + VaultLoginOperationImpl(client, vaultUri) } @@ -232,9 +217,8 @@ object Vault { } } - def loginAndKeepSecretLeased[F[_]: Temporal, A: Decoder](client: Client[F], vaultUri: Uri) - (roleId: String, roleSecretId: Option[String], secretPath: String, duration: FiniteDuration, waitInterval: FiniteDuration): Stream[F, A] = - Stream.eval(login(client, vaultUri)(roleId, roleSecretId)).flatMap(token => keepLoginAndSecretLeased[F, A](client, vaultUri)(token, secretPath, duration, waitInterval)) + def loginAndKeepSecretLeased[F[_], A](client: Client[F], vaultUri: Uri): VaultLoginAndKeepSecretLeasedOperation[F, A] = + VaultLoginAndKeepSecretLeasedOperationImpl(client, vaultUri) def loginK8sAndKeepSecretLeased[F[_]: Temporal, A: Decoder](client: Client[F], vaultUri: Uri) (roleId: String, jwt: String, secretPath: String, duration: FiniteDuration, waitInterval: FiniteDuration, loginMountPoint: Uri.Path = path"/auth/kubernetes" ): Stream[F, A] = @@ -274,9 +258,9 @@ object Vault { } } - def loginAndKeep[F[_]: Async](client: Client[F], vaultUri: Uri) - (roleId: String, tokenLeaseExtension: FiniteDuration): Stream[F, String] = - Stream.eval(login(client, vaultUri)(roleId)).flatMap(token => keepLoginRenewed[F](client, vaultUri)(token, tokenLeaseExtension)) + def loginAndKeep[F[_]](client: Client[F], vaultUri: Uri): VaultLoginAndKeepOperation[F] = { + VaultLoginAndKeepOperationImpl(client, vaultUri) + } /** @@ -338,4 +322,59 @@ object Vault { override def getMessage(): String = s"Token lease $leaseId could not be renewed any longer" } + trait VaultLoginOperation[F[_]] { + def withRoleSecretId(roleSecretId: String): VaultLoginOperation[F] + def apply(roleId: String)(implicit F: Concurrent[F]): F[VaultToken] + } + + final case class VaultLoginOperationImpl[F[_]](client: Client[F], vaultUri: Uri, roleSecretId: Option[String] = None) extends VaultLoginOperation[F] { + override def withRoleSecretId(roleSecretId: String): VaultLoginOperation[F] = copy(roleSecretId = Some(roleSecretId)) + override def apply(roleId: String)(implicit F: Concurrent[F]): F[VaultToken] = { + val request = Request[F]( + method = Method.POST, + uri = vaultUri / "v1" / "auth" / "approle" / "login" + ).withEntity( + Json.fromFields( + Seq("role_id" -> Json.fromString(roleId)) ++ + roleSecretId.fold(Seq[(String, Json)]())(sId => Seq("secret_id" -> Json.fromString(sId))) + ) + ) + for { + json <- F.handleErrorWith(client.expect[Json](request) + ) { e => + F.raiseError(VaultRequestError(request, e.some, s"roleId=$roleId, roleSecretId=$roleSecretId".some)) + } + token <- raiseKnownError(json.hcursor.get[VaultToken]("auth"))(decoderError) + } yield token + } + } + + trait VaultLoginAndKeepOperation[F[_]] { + def withRoleSecretId(roleSecretId: String): VaultLoginAndKeepOperation[F] + def apply(roleId: String, tokenLeaseExtension: FiniteDuration)(implicit A: Async[F]): Stream[F, String] + } + + final case class VaultLoginAndKeepOperationImpl[F[_]](client: Client[F], vaultUri: Uri, roleSecretId: Option[String] = None) extends VaultLoginAndKeepOperation[F] { + override def withRoleSecretId(roleSecretId: String): VaultLoginAndKeepOperation[F] = copy(roleSecretId = Some(roleSecretId)) + def apply(roleId: String, tokenLeaseExtension: FiniteDuration)(implicit A: Async[F]): Stream[F, String] = { + val loginOperation = login(client, vaultUri) + Stream.eval(roleSecretId.fold(loginOperation)(loginOperation.withRoleSecretId)(roleId)) + .flatMap(token => keepLoginRenewed[F](client, vaultUri)(token, tokenLeaseExtension)) + } + } + + trait VaultLoginAndKeepSecretLeasedOperation[F[_], A] { + def withRoleSecretId(roleSecretId: String): VaultLoginAndKeepSecretLeasedOperation[F, A] + def apply(roleId: String, secretPath: String, duration: FiniteDuration, waitInterval: FiniteDuration)(implicit T: Temporal[F], D: Decoder[A]): Stream[F, A] + } + + final case class VaultLoginAndKeepSecretLeasedOperationImpl[F[_], A](client: Client[F], vaultUri: Uri, roleSecretId: Option[String] = None) extends VaultLoginAndKeepSecretLeasedOperation[F, A] { + override def withRoleSecretId(roleSecretId: String): VaultLoginAndKeepSecretLeasedOperation[F, A] = copy(roleSecretId = Some(roleSecretId)) + override def apply(roleId: String, secretPath: String, duration: FiniteDuration, waitInterval: FiniteDuration)(implicit T: Temporal[F], D: Decoder[A]): Stream[F, A] = { + val loginOperation = login(client, vaultUri) + Stream.eval(roleSecretId.fold(loginOperation)(loginOperation.withRoleSecretId)(roleId)) + .flatMap(token => keepLoginAndSecretLeased[F, A](client, vaultUri)(token, secretPath, duration, waitInterval)) + } + } + } diff --git a/core/src/test/scala/com/banno/vault/VaultSpec.scala b/core/src/test/scala/com/banno/vault/VaultSpec.scala index 439bf9eb..751dd434 100644 --- a/core/src/test/scala/com/banno/vault/VaultSpec.scala +++ b/core/src/test/scala/com/banno/vault/VaultSpec.scala @@ -91,11 +91,11 @@ class VaultSpec extends CatsEffectSuite with ScalaCheckEffectSuite with MissingP val private_key_type: String = UUID.randomUUID().toString val serial_number: String = UUID.randomUUID().toString - val validRoleId: String = UUID.randomUUID().toString - val validRoleIdAndRoleSecretId: (String, Option[String]) = UUID.randomUUID().toString -> Some(UUID.randomUUID().toString) - val invalidJSONRoleId: String = UUID.randomUUID().toString - val roleIdWithoutToken: String = UUID.randomUUID().toString - val roleIdWithoutLease: String = UUID.randomUUID().toString + val validRoleId: String = UUID.randomUUID().toString + val validRoleIdAndRoleSecretId: (String, String) = UUID.randomUUID().toString -> UUID.randomUUID().toString + val invalidJSONRoleId: String = UUID.randomUUID().toString + val roleIdWithoutToken: String = UUID.randomUUID().toString + val roleIdWithoutLease: String = UUID.randomUUID().toString val validKubernetesRole: String = UUID.randomUUID().toString val validKubernetesJwt: String = Random.alphanumeric.take(20).mkString //simulate a signed jwt https://www.vaultproject.io/api/auth/kubernetes/index.html#login @@ -168,7 +168,7 @@ class VaultSpec extends CatsEffectSuite with ScalaCheckEffectSuite with MissingP | "renewable": $renewable | } |}""".stripMargin) - case roleIdAndRoleSecretId if roleIdAndRoleSecretId.roleId == validRoleIdAndRoleSecretId._1 && roleIdAndRoleSecretId.roleSecretId == validRoleIdAndRoleSecretId._2 => + case roleIdAndRoleSecretId if roleIdAndRoleSecretId.roleId == validRoleIdAndRoleSecretId._1 && roleIdAndRoleSecretId.roleSecretId == Some(validRoleIdAndRoleSecretId._2) => Ok(s""" |{ | "auth": { @@ -322,7 +322,7 @@ class VaultSpec extends CatsEffectSuite with ScalaCheckEffectSuite with MissingP test("login works as expected when sending a valid roleId and roleSecretId") { PropF.forAllF(VaultArbitraries.validVaultUri) { uri => - Vault.login(mockClient, uri)(validRoleIdAndRoleSecretId._1, validRoleIdAndRoleSecretId._2).assertEquals(validToken) + Vault.login(mockClient, uri).withRoleSecretId(validRoleIdAndRoleSecretId._2)(validRoleIdAndRoleSecretId._1).assertEquals(validToken) } } @@ -504,7 +504,22 @@ class VaultSpec extends CatsEffectSuite with ScalaCheckEffectSuite with MissingP Arbitrary.arbitrary[FiniteDuration] ) { case (uri, leaseDuration, waitInterval) => PropF.boolean[IO](leaseDuration < waitInterval) ==> { - Vault.loginAndKeepSecretLeased[IO, Unit](mockClient, uri)(validRoleId, None, "", leaseDuration, waitInterval) + Vault.loginAndKeepSecretLeased[IO, Unit](mockClient, uri)(validRoleId, "", leaseDuration, waitInterval) + .attempt + .compile + .last + .assertEquals(Some(Left(Vault.InvalidRequirement("waitInterval longer than requested Lease Duration")))) + }} + } + + test("loginAndKeepSecretLeased with roleSecretId fails when wait duration is longer than lease duration") { + PropF.forAllF( + VaultArbitraries.validVaultUri, + Arbitrary.arbitrary[FiniteDuration], + Arbitrary.arbitrary[FiniteDuration] + ) { case (uri, leaseDuration, waitInterval) => PropF.boolean[IO](leaseDuration < waitInterval) ==> { + + Vault.loginAndKeepSecretLeased[IO, Unit](mockClient, uri).withRoleSecretId(validRoleIdAndRoleSecretId._2)(validRoleIdAndRoleSecretId._1, "", leaseDuration, waitInterval) .attempt .compile .last