rustlavel_vault/
leases.rs1use crate::client::VaultClient;
15use crate::error::VaultError;
16use rustlavel_core::Result;
17use crate::lease::Lease;
18use rustlavel_core::Json;
19use std::time::Duration;
20
21impl VaultClient {
22 pub async fn renew_lease(&self, lease: &Lease, increment: Option<Duration>) -> Result<Lease> {
30 if lease.id().is_empty() {
31 return Err(VaultError::BadRequest(
32 "this lease has no id, so it is a token's. Renew it with `renew_token`.".into(),
33 )
34 .into());
35 }
36
37 let mut body = vec![("lease_id".to_string(), Json::from(lease.id()))];
38 if let Some(increment) = increment {
39 body.push(("increment".into(), Json::Number(increment.as_secs() as f64)));
40 }
41
42 let response = self.post("sys/leases/renew", Json::object(body)).await?;
43 Ok(lease.renewed(response.lease.duration(), response.lease.renewable()))
44 }
45
46 pub async fn revoke_lease(&self, lease: &Lease) -> Result<()> {
51 if lease.id().is_empty() {
52 return Err(VaultError::BadRequest(
53 "this lease has no id, so it is a token's. Revoke it with `revoke_token`.".into(),
54 )
55 .into());
56 }
57
58 self.post("sys/leases/revoke", Json::object([("lease_id", Json::from(lease.id()))]))
59 .await
60 .map(|_| ())
61 }
62
63 pub async fn lookup_lease(&self, lease_id: &str) -> Result<Lease> {
69 let response =
70 self.post("sys/leases/lookup", Json::object([("lease_id", Json::from(lease_id))]))
71 .await?;
72 let data = &response.data;
73
74 let seconds = data.get("ttl").and_then(Json::as_i64).unwrap_or(0).max(0);
75 let renewable = data.get("renewable").and_then(Json::as_bool).unwrap_or(false);
76
77 Ok(Lease::new(lease_id, Duration::from_secs(seconds as u64), renewable))
78 }
79
80 pub async fn renew_token(&self, increment: Option<Duration>) -> Result<Lease> {
86 let mut body = Vec::new();
87 if let Some(increment) = increment {
88 body.push(("increment".to_string(), Json::Number(increment.as_secs() as f64)));
89 }
90
91 let response = self.post("auth/token/renew-self", Json::object(body)).await?;
92 Ok(response.lease)
93 }
94
95 pub async fn revoke_token(&self) -> Result<()> {
102 let result = self.post("auth/token/revoke-self", Json::Null).await.map(|_| ());
103 self.clear_token();
104 result
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use rustlavel_client::{Fake, FakeResponse};
112
113 fn vault(fake: Fake) -> VaultClient {
114 VaultClient::new("https://vault.test:8200").faking(fake)
115 }
116
117 fn lease() -> Lease {
118 Lease::new("database/creds/app-readwrite/5p1PU", Duration::from_secs(3600), true)
119 }
120
121 #[tokio::test]
122 async fn renewing_a_lease_takes_the_duration_vault_actually_granted() {
123 let capped = FakeResponse::json(
126 Json::parse(
127 r#"{"lease_id":"database/creds/app-readwrite/5p1PU",
128 "renewable":true,"lease_duration":3600,"data":null,"auth":null}"#,
129 )
130 .unwrap(),
131 );
132 let client = vault(Fake::new().fallback(capped));
133
134 let renewed =
135 client.renew_lease(&lease(), Some(Duration::from_secs(7200))).await.unwrap();
136
137 assert_eq!(renewed.duration(), Duration::from_secs(3600));
138 assert_eq!(renewed.id(), "database/creds/app-readwrite/5p1PU");
139 assert!(!renewed.should_renew());
140
141 let sent = client.fake().unwrap().recorded()[0].json().unwrap();
142 assert_eq!(sent.get("lease_id").unwrap().as_str(), Some("database/creds/app-readwrite/5p1PU"));
143 assert_eq!(sent.get("increment").unwrap().as_i64(), Some(7200));
144 client.fake().unwrap().assert_sent("/v1/sys/leases/renew");
145 }
146
147 #[tokio::test]
148 async fn revoking_a_lease_sends_its_id_and_accepts_no_content() {
149 let client = vault(Fake::new().fallback(FakeResponse::text("").status(204)));
150
151 client.revoke_lease(&lease()).await.unwrap();
152
153 client.fake().unwrap().assert_sent("/v1/sys/leases/revoke");
154 let sent = client.fake().unwrap().recorded()[0].json().unwrap();
155 assert!(sent.get("lease_id").is_some());
156 }
157
158 #[tokio::test]
159 async fn a_token_lease_is_sent_to_the_right_endpoint_or_refused() {
160 let client = vault(Fake::new().fallback(FakeResponse::text("").status(204)));
164
165 let error = client.renew_lease(&Lease::none(), None).await.unwrap_err().to_string();
166 assert!(error.contains("renew_token"), "{error}");
167
168 let error = client.revoke_lease(&Lease::none()).await.unwrap_err().to_string();
169 assert!(error.contains("revoke_token"), "{error}");
170
171 client.fake().unwrap().assert_count(0);
172 }
173
174 #[tokio::test]
175 async fn renewing_the_token_reads_the_lease_back_from_auth() {
176 let renewed = FakeResponse::json(
179 Json::parse(
180 r#"{"lease_id":"","renewable":false,"lease_duration":0,"data":null,
181 "auth":{"client_token":"s.RpkkBIsBT61V1PTDZGP6eE4q",
182 "lease_duration":7200,"renewable":true}}"#,
183 )
184 .unwrap(),
185 );
186 let client = vault(Fake::new().fallback(renewed));
187 client.set_token("s.RpkkBIsBT61V1PTDZGP6eE4q");
188
189 let lease = client.renew_token(Some(Duration::from_secs(7200))).await.unwrap();
190
191 assert_eq!(lease.duration(), Duration::from_secs(7200));
192 assert!(lease.renewable());
193 client.fake().unwrap().assert_sent("/v1/auth/token/renew-self");
194 }
195
196 #[tokio::test]
197 async fn revoking_the_token_also_forgets_it() {
198 let client = vault(Fake::new().fallback(FakeResponse::text("").status(204)));
201 client.set_token("s.doomed");
202
203 client.revoke_token().await.unwrap();
204
205 assert!(!client.has_token());
206 client.fake().unwrap().assert_sent("/v1/auth/token/revoke-self");
207 }
208
209 #[tokio::test]
210 async fn looking_a_lease_up_reports_what_the_server_believes() {
211 let body = FakeResponse::json(
212 Json::parse(
213 r#"{"data":{"expire_time":"2026-08-29T14:49:47Z",
214 "id":"database/creds/app-readwrite/5p1PU",
215 "issue_time":"2026-08-29T13:49:47Z","last_renewal":null,
216 "path":"database/creds/app-readwrite","renewable":true,"ttl":3600}}"#,
217 )
218 .unwrap(),
219 );
220 let client = vault(Fake::new().fallback(body));
221
222 let lease = client.lookup_lease("database/creds/app-readwrite/5p1PU").await.unwrap();
223
224 assert_eq!(lease.duration(), Duration::from_secs(3600));
225 assert!(lease.renewable());
226 client.fake().unwrap().assert_sent("/v1/sys/leases/lookup");
227 }
228
229 #[tokio::test]
230 async fn a_revoked_lease_can_no_longer_be_looked_up() {
231 let gone = FakeResponse::text(r#"{"errors":["invalid lease"]}"#).status(400);
233 let client = vault(Fake::new().fallback(gone));
234
235 let error = client.lookup_lease("database/creds/app/gone").await.unwrap_err();
236
237 let error = error.to_string();
241 assert!(error.contains("invalid lease"), "{error}");
242 assert!(!error.contains("nothing is stored at"), "{error}");
243 }
244}