11package com .drobisch .tresor .vault
22
3- import java .util .concurrent .TimeUnit
4-
53import cats .data .ReaderT
4+ import cats .effect .concurrent .Ref
5+ import cats .effect .{Clock , IO , Sync }
6+ import cats .syntax .apply ._
67import cats .syntax .flatMap ._
78import cats .syntax .functor ._
8- import cats .syntax .apply ._
9- import cats .effect .{Clock , Sync }
10- import cats .effect .concurrent .Ref
119import com .drobisch .tresor .Provider
12- import sttp .client3 ._
13- import io .circe .Json
10+ import io .circe .{Encoder , Json }
1411import io .circe .generic .auto ._
12+ import io .circe .syntax ._
1513import org .slf4j .LoggerFactory
14+ import sttp .client3 ._
15+
16+ import java .util .concurrent .TimeUnit
1617
1718private [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}
0 commit comments