Skip to content

Commit 42df080

Browse files
author
Andreas Drobisch
committed
(feature): wip: mongo engine and refactoring
1 parent b5bb2e9 commit 42df080

File tree

13 files changed

+206
-97
lines changed

13 files changed

+206
-97
lines changed

docker-compose.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
version: "3.8"
2+
3+
services:
4+
vault-server:
5+
network_mode: "host"
6+
image: vault:latest
7+
environment:
8+
VAULT_ADDR: "http://0.0.0.0:8200"
9+
VAULT_DEV_ROOT_TOKEN_ID: "vault-plaintext-root-token"
10+
cap_add:
11+
- IPC_LOCK
12+
mongo:
13+
network_mode: "host"
14+
image: mongo

src/main/scala/com/drobisch/tresor/vault/AWS.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ final case class AwsContext(
1919
* @tparam F
2020
* effect type to use
2121
*/
22-
class AWS[F[_]](implicit sync: Sync[F], clock: Clock[F])
23-
extends SecretEngineProvider[F, (AwsContext, VaultConfig)] {
22+
class AWS[F[_]](val path: String)(implicit sync: Sync[F], clock: Clock[F])
23+
extends SecretEngineProvider[F, (AwsContext, VaultConfig), Nothing] {
2424

2525
/** create a aws engine credential
2626
*
@@ -37,10 +37,10 @@ class AWS[F[_]](implicit sync: Sync[F], clock: Clock[F])
3737
sync.flatMap(sync.delay {
3838
val roleArnPart = awsContext.roleArn.map(s"&role_arn=" + _).getOrElse("")
3939
val ttlPart = "&ttl=" + awsContext.ttlString.getOrElse("3600s")
40-
val infixPart = if (awsContext.useSts) "sts" else "creds"
40+
val stsOrCreds = if (awsContext.useSts) "sts" else "creds"
4141

4242
val requestUri =
43-
s"${vaultConfig.apiUrl}/aws/$infixPart/${awsContext.name}?$roleArnPart$ttlPart"
43+
s"${vaultConfig.apiUrl}/$path/$stsOrCreds/${awsContext.name}?$roleArnPart$ttlPart"
4444

4545
basicRequest
4646
.get(uri"$requestUri")
@@ -60,5 +60,5 @@ class AWS[F[_]](implicit sync: Sync[F], clock: Clock[F])
6060
}
6161

6262
object AWS {
63-
def apply[F[_]](implicit sync: Sync[F], clock: Clock[F]) = new AWS[F]
63+
def apply[F[_]](path: String)(implicit sync: Sync[F], clock: Clock[F]) = new AWS[F](path)
6464
}

src/main/scala/com/drobisch/tresor/vault/Database.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.drobisch.tresor.vault
22

33
import cats.effect.{Clock, Sync}
44
import com.drobisch.tresor.Secret
5+
import io.circe.Json
56
import sttp.client3._
67

78
final case class DatabaseContext(role: String)
@@ -13,8 +14,8 @@ final case class DatabaseContext(role: String)
1314
* @tparam F
1415
* context type to use
1516
*/
16-
class Database[F[_]](implicit sync: Sync[F], clock: Clock[F])
17-
extends SecretEngineProvider[F, (DatabaseContext, VaultConfig)] {
17+
class Database[F[_]](val path: String)(implicit sync: Sync[F], clock: Clock[F])
18+
extends SecretEngineProvider[F, (DatabaseContext, VaultConfig), Json] {
1819

1920
/** read the credentials for a DB role
2021
*
@@ -29,7 +30,7 @@ class Database[F[_]](implicit sync: Sync[F], clock: Clock[F])
2930
val (db, vaultConfig) = context
3031

3132
val response = basicRequest
32-
.get(uri"${vaultConfig.apiUrl}/database/creds/${db.role}")
33+
.get(uri"${vaultConfig.apiUrl}/$path/creds/${db.role}")
3334
.header("X-Vault-Token", vaultConfig.token)
3435
.send(backend)
3536

@@ -40,5 +41,5 @@ class Database[F[_]](implicit sync: Sync[F], clock: Clock[F])
4041
}
4142

4243
object Database {
43-
def apply[F[_]](implicit sync: Sync[F], clock: Clock[F]) = new Database[F]
44+
def apply[F[_]](path: String)(implicit sync: Sync[F], clock: Clock[F]) = new Database[F](path)
4445
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
package com.drobisch.tresor.vault
22

3+
import cats.effect.Sync
4+
import io.circe.Json
35
import sttp.client3._
46

57
trait HttpSupport {
68
protected lazy val backend = HttpURLConnectionBackend()
9+
10+
implicit class ResponseOps[F[_]](response: Response[Either[String, String]])(implicit sync: Sync[F]) {
11+
def parseJson: F[Json] = sync.fromEither(
12+
response.body.left
13+
.map(httpError => new RuntimeException(s"error during http request: $httpError ($response)"))
14+
.flatMap(bodyString => io.circe.parser.decode[Json](bodyString).left.map(parseError => new RuntimeException(s"unable to parse json from $bodyString: $parseError"))))
15+
}
716
}

src/main/scala/com/drobisch/tresor/vault/KV.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ final case class KeyValueContext(key: String)
1313
* @tparam F
1414
* effect type to use
1515
*/
16-
class KV[F[_]](implicit sync: Sync[F], clock: Clock[F])
17-
extends SecretEngineProvider[F, (KeyValueContext, VaultConfig)] {
16+
class KV[F[_]](val path: String)(implicit sync: Sync[F], clock: Clock[F])
17+
extends SecretEngineProvider[F, (KeyValueContext, VaultConfig), Nothing] {
1818

1919
/** read the secret from a path
2020
*
@@ -29,7 +29,7 @@ class KV[F[_]](implicit sync: Sync[F], clock: Clock[F])
2929
val (kv, vaultConfig) = context
3030

3131
val response = basicRequest
32-
.get(uri"${vaultConfig.apiUrl}/secret/${kv.key}")
32+
.get(uri"${vaultConfig.apiUrl}/$path/${kv.key}")
3333
.header("X-Vault-Token", vaultConfig.token)
3434
.send(backend)
3535

@@ -40,5 +40,5 @@ class KV[F[_]](implicit sync: Sync[F], clock: Clock[F])
4040
}
4141

4242
object KV {
43-
def apply[F[_]](implicit sync: Sync[F], clock: Clock[F]) = new KV[F]
43+
def apply[F[_]](path: String)(implicit sync: Sync[F], clock: Clock[F]) = new KV[F](path)
4444
}
Lines changed: 86 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
package com.drobisch.tresor.vault
22

3-
import java.util.concurrent.TimeUnit
4-
53
import cats.data.ReaderT
4+
import cats.effect.concurrent.Ref
5+
import cats.effect.{Clock, IO, Sync}
6+
import cats.syntax.apply._
67
import cats.syntax.flatMap._
78
import cats.syntax.functor._
8-
import cats.syntax.apply._
9-
import cats.effect.{Clock, Sync}
10-
import cats.effect.concurrent.Ref
119
import com.drobisch.tresor.Provider
12-
import sttp.client3._
13-
import io.circe.Json
10+
import io.circe.{Encoder, Json}
1411
import io.circe.generic.auto._
12+
import io.circe.syntax._
1513
import org.slf4j.LoggerFactory
14+
import sttp.client3._
15+
16+
import java.util.concurrent.TimeUnit
1617

1718
private[vault] final case class LeaseDTO(
1819
lease_id: Option[String],
@@ -21,13 +22,37 @@ private[vault] final case class LeaseDTO(
2122
data: Option[Map[String, Option[String]]]
2223
)
2324

24-
abstract class SecretEngineProvider[Effect[_], ProviderContext](implicit
25+
abstract class SecretEngineProvider[Effect[_], ProviderContext, Config](implicit
2526
sync: Sync[Effect],
2627
clock: Clock[Effect]
2728
) extends Provider[Effect, ProviderContext, Lease]
2829
with HttpSupport {
2930
protected val log = LoggerFactory.getLogger(getClass)
3031

32+
/**
33+
* the path at which this engine is mounted
34+
* @return
35+
*/
36+
def path: String
37+
38+
/**
39+
* write a config for this engine path
40+
* @param config
41+
*/
42+
def writeConfig(name: String, config: Config)(implicit encoder: Encoder[Config]): ReaderT[Effect, VaultConfig, Json] = for {
43+
response <-
44+
ReaderT[Effect, VaultConfig, Response[Either[String, String]]] { vaultConfig =>
45+
val request = basicRequest
46+
.post(uri"${vaultConfig.apiUrl}/$path/config/$name")
47+
.body(config.asJson.noSpaces)
48+
.header("X-Vault-Token", vaultConfig.token)
49+
.send(backend)
50+
51+
sync.delay(request)
52+
}
53+
json <- ReaderT.liftF(response.parseJson)
54+
} yield json
55+
3156
/** renew a lease
3257
*
3358
* https://www.vaultproject.io/api/system/leases.html#renew-lease
@@ -36,6 +61,7 @@ abstract class SecretEngineProvider[Effect[_], ProviderContext](implicit
3661
* a lease
3762
* @param increment
3863
* time to extend the lease in seconds
64+
* (depending on the engine, this might be treated as the new ttl of the lease, this is the case for Mongo Atlas)
3965
* @return
4066
* reader for an extended lease
4167
*/
@@ -44,40 +70,52 @@ abstract class SecretEngineProvider[Effect[_], ProviderContext](implicit
4470
increment: Option[Long]
4571
): ReaderT[Effect, VaultConfig, Lease] = lease.leaseId
4672
.map { leaseId =>
47-
ReaderT[Effect, VaultConfig, Lease] { vaultConfig =>
48-
sync.flatMap(sync.delay {
49-
basicRequest
50-
.post(uri"${vaultConfig.apiUrl}/sys/leases/renew")
51-
.body(
52-
Json
53-
.obj(
54-
"lease_id" -> Json.fromString(leaseId),
55-
"increment" -> Json.fromLong(increment.getOrElse(3600))
56-
)
57-
.noSpaces
58-
)
59-
.header("X-Vault-Token", vaultConfig.token)
60-
.send(backend)
61-
}) { response =>
62-
log.debug("response from vault: {}", response)
63-
parseLease(response).map(renewed => renewed.copy(data = lease.data))
73+
for {
74+
now <- ReaderT.liftF(clock.realTime(TimeUnit.SECONDS))
75+
response <- ReaderT[Effect, VaultConfig, Response[Either[String, String]]] { vaultConfig =>
76+
sync.delay {
77+
basicRequest
78+
.post(uri"${vaultConfig.apiUrl}/sys/leases/renew")
79+
.body(
80+
Json
81+
.obj(
82+
"lease_id" -> Json.fromString(leaseId),
83+
"increment" -> Json.fromLong(increment.getOrElse(3600))
84+
)
85+
.noSpaces
86+
)
87+
.header("X-Vault-Token", vaultConfig.token)
88+
.send(backend)
89+
}
6490
}
65-
}
91+
lease <- {
92+
ReaderT.liftF {
93+
log.debug("response from vault: {}", response)
94+
parseLease(response).map { renewed =>
95+
renewed.copy(
96+
data = lease.data,
97+
creationTime = lease.creationTime,
98+
lastRenewalTime = Some(now)
99+
)
100+
}
101+
}
102+
}
103+
} yield lease
66104
}
67105
.getOrElse(
68106
ReaderT[Effect, VaultConfig, Lease](_ =>
69107
sync.raiseError(new IllegalArgumentException("no lease id defined"))
70108
)
71109
)
72110

73-
/** Auto-refresh a lease reference based on the current time.
111+
/** Refresh a lease reference based on the current time.
74112
*
75113
* This is not a continuous refresh, the flow is:
76114
*
77-
* 1. if lease is not renewable or is expired: create a new lease 2. if its
78-
* not expired but the current time is greater then issue time * refresh
79-
* ratio: refresh the current lease 3. return the current lease
80-
* otherwise
115+
* 1. if lease is not renewable: create a new lease
116+
* 2. if its not expired but the current time is greater then issue time * refresh ratio:
117+
* refresh the current lease
118+
* 3. return the current lease otherwise
81119
*
82120
* @param leaseRef
83121
* a reference to the current (maybe empty) lease
@@ -91,33 +129,34 @@ abstract class SecretEngineProvider[Effect[_], ProviderContext](implicit
91129
* @return
92130
* an effect reader with the logic above applied
93131
*/
94-
def autoRefresh(
95-
leaseRef: Ref[Effect, Option[Lease]],
96-
increment: Option[Long] = None,
97-
refreshRatio: Double = 0.5,
98-
forceNew: Boolean = false
99-
)(
100-
newLease: ReaderT[Effect, VaultConfig, Lease]
132+
def refresh(leaseRef: Ref[Effect, Option[Lease]],
133+
refreshRatio: Double = 0.5)(
134+
create: ReaderT[Effect, VaultConfig, Lease],
135+
renew: Lease => ReaderT[Effect, VaultConfig, Lease],
136+
maxTtlSeconds: ReaderT[Effect, VaultConfig, Long] = ReaderT.liftF(sync.pure(3600)),
137+
maxReached: ReaderT[Effect, VaultConfig, Unit] = ReaderT.liftF(sync.unit)
101138
): ReaderT[Effect, VaultConfig, Lease] = {
102139
for {
103140
now <- ReaderT.liftF(clock.realTime(TimeUnit.SECONDS))
104141
currentLease <- ReaderT.liftF(leaseRef.get)
142+
max <- maxTtlSeconds
105143
valid <- {
106144
currentLease match {
107145
case Some(lease) =>
108146
val duration = lease.leaseDuration.getOrElse(0L)
109-
val expiryTime = lease.issueTime + duration
147+
val expiryTime = lease.lastRenewalTime.getOrElse(lease.creationTime) + duration
110148
val ratioTime = expiryTime - duration * refreshRatio
149+
val totalDuration = lease.totalLeaseDuration(now)
111150

112-
if (!lease.renewable || now >= expiryTime) {
113-
newLease
114-
} else if (now >= ratioTime && !forceNew) {
115-
renew(lease, increment)
116-
} else if (now >= ratioTime && forceNew) {
117-
newLease
151+
if (!lease.renewable) {
152+
create
153+
} else if (totalDuration >= max) {
154+
maxReached.flatMapF(_ => sync.pure(lease))
155+
} else if (now >= ratioTime) {
156+
renew(lease)
118157
} else ReaderT[Effect, VaultConfig, Lease](_ => sync.pure(lease))
119158

120-
case None => newLease
159+
case None => create
121160
}
122161
}
123162
updated <- ReaderT[Effect, VaultConfig, Lease](_ =>
@@ -145,7 +184,7 @@ abstract class SecretEngineProvider[Effect[_], ProviderContext](implicit
145184
data = dto.data.getOrElse(Map.empty),
146185
renewable = dto.renewable,
147186
leaseDuration = dto.lease_duration,
148-
issueTime = now
187+
creationTime = now
149188
)
150189
}
151190
}

src/main/scala/com/drobisch/tresor/vault/package.scala

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.drobisch.tresor
22

3-
/** basic types to implement secrets coming from https://www.vaultproject.io
3+
/**
4+
* basic types to implement secrets coming from https://www.vaultproject.io
45
*/
56
package object vault {
67
final case class VaultConfig(apiUrl: String, token: String)
@@ -17,14 +18,21 @@ package object vault {
1718
* true if the lease validation period can be extends
1819
* @param leaseDuration
1920
* duration of the lease starting with creation
21+
* @param creationTime
22+
* the time of creation of the lease
23+
* @param totalLeaseDuration
24+
* the total time of the lease
2025
*/
2126
final case class Lease(
22-
leaseId: Option[String],
23-
data: Map[String, Option[String]],
24-
renewable: Boolean,
25-
leaseDuration: Option[Long],
26-
issueTime: Long
27-
)
27+
leaseId: Option[String],
28+
data: Map[String, Option[String]],
29+
renewable: Boolean,
30+
leaseDuration: Option[Long],
31+
creationTime: Long,
32+
lastRenewalTime: Option[Long] = None
33+
) {
34+
def totalLeaseDuration(now: Long): Long = now - creationTime
35+
}
2836

2937
implicit object VaultSecretLease extends Secret[Lease] {
3038
override def data(secret: Lease): Option[Map[String, Option[String]]] =
@@ -34,7 +42,7 @@ package object vault {
3442
override def validDuration(secret: Lease): Option[Long] =
3543
secret.leaseDuration
3644
override def creationTime(secret: Lease): Option[Long] = Some(
37-
secret.issueTime
45+
secret.creationTime
3846
)
3947
}
4048
}

0 commit comments

Comments
 (0)