Infrastructure

HashiCorp Vault

HashiCorp Vault's HTTP API: KV v2 secrets with versions, check-and-set, patch, soft delete and destroy, AppRole sign-in, transit encryption, decryption, rewrapping and batches, and dynamic secrets' leases read, renewed and revoked.

Version 1.0.0 · 21 functions · 3 crates

#secrets#encryption#kms#leases

Functions

What use vault.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use vault_login, vault_health from vault (Quillo 0.6).

vault_login() → anyor an error

Signs in by AppRole now, from VAULT_ROLE_ID and VAULT_SECRET_ID, and gives { ttl_seconds }. Calls do it themselves when they need to; this is for finding a bad secret id at start.

vault_health() → anyor an error

Vault's state from /sys/health, which answers by status code: { initialized, sealed, standby, version }, a standby or a sealed server being an answer, not an error.

vault_kv_get(path: str, version: int) → anyor an error

Reads a KV v2 secret; version 0 for the newest. Gives { data, version, created_time, deleted_time, destroyed }, data null for a version deleted or destroyed, or null when there is no such secret.

vault_kv_put(path: str, data: any, cas: int) → anyor an error

Writes a new version of a secret (a map). cas is -1 for no check, 0 to write only if there is none, or the version it must still be at. Gives { version, created_time }. A failed check is an error saying so.

vault_kv_patch(path: str, changes: any, cas: int) → anyor an error

Merges fields into the newest version as a new version, others staying: JSON merge patch, a field given as null being removed. Needs the patch capability. Gives { version, created_time }.

vault_kv_delete(path: str, versions: any) → boolor an error

Soft-deletes versions (a list), or the newest when versions is null. They can come back with vault_kv_undelete.

vault_kv_undelete(path: str, versions: any) → boolor an error

Brings soft-deleted versions back.

vault_kv_destroy(path: str, versions: any) → boolor an error

Destroys versions for good: their data cannot come back.

vault_kv_metadata(path: str) → anyor an error

A secret's metadata: { current_version, oldest_version, versions: { "1": { created_time, deletion_time, destroyed } }, ... }, or null.

vault_kv_list(path: str) → anyor an error

The names under a folder of secrets, folders ending in /. None is an empty list.

vault_encrypt(key: str, plaintext: str, context: str) → stror an error

Encrypts text with a transit key, which stays in Vault. context is "" or the derivation context of a derived key. Gives vault:v1:..., to keep in a database.

vault_decrypt(key: str, ciphertext: str, context: str) → stror an error

Decrypts what vault_encrypt gave, back to text.

vault_rewrap(key: str, ciphertext: str) → stror an error

Re-encrypts ciphertext under the key's newest version, without showing the plaintext to anyone: for after vault_rotate.

vault_encrypt_many(key: str, texts: list) → anyor an error

Encrypts many texts in one call. One bad item does not fail the rest: gives a list in order, each { ciphertext } or { error }.

vault_decrypt_many(key: str, ciphertexts: list) → listor an error

Decrypts many ciphertexts in one call: a list in order, each { text } or { error }.

vault_rotate(key: str) → boolor an error

Adds a new version to a transit key: new encryptions use it, and old ciphertext still decrypts.

vault_read(path: str) → anyor an error

Reads any path, such as database/creds/readonly for dynamic credentials: { data, lease_id, lease_duration, renewable, warnings }, or null on a 404.

vault_write(path: str, data: any) → anyor an error

Writes to any path with a map, for what this plugin has no function for; gives Vault's answer.

vault_lease_renew(lease_id: str, increment_seconds: int) → anyor an error

Renews a lease by increment_seconds (0 for its default). Gives { lease_id, lease_duration, renewable }: Vault may give less than asked, near its maximum.

vault_lease_lookup(lease_id: str) → anyor an error

What a lease has left: { id, ttl, expire_time, renewable }.

vault_lease_revoke(lease_id: str) → boolor an error

Ends a lease now, and with it what it stands for, a database user say.

Get it

$ quillo install vault               # vault.qwer into this folder$ quillo install vault --example     # and its example, vault.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/vault.qwer$ curl -O https://quillolang.org/plugins/files/vault.qwe

Then, in your program, all of it or only what you call:

main.qwe
use vault.qweruse vault_login, vault_health from vault      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
ureq = "2"base64 = "0.22"serde_json = { version = "1", features = ["preserve_order"] }

Example

vault.qwe
// Secrets from HashiCorp Vault: a secret and its versions, an AppRole sign-in, data encrypted by a// key that never leaves Vault, and short-lived database credentials under a lease.////   VAULT_API_URL=http://localhost:8200 VAULT_ROLE_ID=... VAULT_SECRET_ID=... quillo run vault.qwe//   then open /health, /kv, /versions, /transit, /leases and /broken//// VAULT_TOKEN=... works in place of AppRole.use "vault.qwer"route GET /health = { health: vault_health()?, login: vault_login()?.ttl_seconds > 0 }route GET /kv {    let one = vault_kv_put("shop/stripe", { api_key: "one", mode: "test" }, 0)?    let read = vault_kv_get("shop/stripe", 0)?    {        version: one.version,        data: read.data,        only_if_new: vault_kv_put("shop/stripe", { api_key: "x" }, 0).error,        none: vault_kv_get("shop/nothing", 0)?,    }}route GET /versions {    let two = vault_kv_put("app/db", { password: "first" }, -1)?    let three = vault_kv_put("app/db", { password: "second" }, two.version)?    let stale = vault_kv_put("app/db", { password: "late" }, two.version).error    let patched = vault_kv_patch("app/db", { user: "app" }, -1)?    let now = vault_kv_get("app/db", 0)?    let old = vault_kv_get("app/db", two.version)?    vault_kv_delete("app/db", [two.version])?    let deleted = vault_kv_get("app/db", two.version)?    vault_kv_undelete("app/db", [two.version])?    let back = vault_kv_get("app/db", two.version)?    vault_kv_destroy("app/db", [three.version])?    let gone = vault_kv_get("app/db", three.version)?    let meta = vault_kv_metadata("app/db")?    {        stale: stale,        now: now.data,        current: patched.version,        old: old.data,        deleted: deleted.data,        deleted_at: deleted.deleted_time != null,        back: back.data,        destroyed: gone.destroyed,        versions: len(keys(meta.versions)),        listed: vault_kv_list("app")?,        top: vault_kv_list("")?,        empty: vault_kv_list("nothing")?,    }}route GET /transit {    let c = vault_encrypt("orders", "card 4242 — é", "")?    let rotated = vault_rotate("orders")?    let d = vault_rewrap("orders", c)?    let many = vault_encrypt_many("orders", ["a", "b"])?    {        prefix: slice(c, 0, 9),        plain: vault_decrypt("orders", c, "")?,        rotated: rotated,        rewrapped: slice(d, 0, 9),        still: vault_decrypt("orders", d, "")?,        many: vault_decrypt_many("orders", [many[0].ciphertext, "vault:v1:bad", many[1].ciphertext])?,        forged: vault_decrypt("orders", "vault:v1:AAAA", "").error,        not_cipher: vault_decrypt("orders", "plain text", "").error,        no_key: vault_encrypt("nothing", "x", "").error,    }}route GET /leases {    let c = vault_read("database/creds/readonly")?    let more = vault_lease_renew(c.lease_id, 600)?    let look = vault_lease_lookup(c.lease_id)?    vault_lease_revoke(c.lease_id)?    { user: c.data.username, renewable: c.renewable, renewed: more.lease_duration, ttl: look.ttl, ended: vault_lease_lookup(c.lease_id).error, no_path: vault_read("no/such/path")? }}route GET /broken = {    forbidden: vault_kv_get("forbidden/key", 0).error,    dots: vault_kv_get("../sys/seal", 0).error,    not_map: vault_kv_put("x", "text", -1).error,}
$ quillo run vault.qwe$ curl 'localhost:8080/health'$ curl 'localhost:8080/kv'$ curl 'localhost:8080/versions'$ curl 'localhost:8080/transit'$ curl 'localhost:8080/leases'$ curl 'localhost:8080/broken'

It talks to the Redis that REDIS_URL names, else Redis on this machine. The site's test runs it against a stand-in.

Source

vault.qwer
//! HashiCorp Vault's HTTP API: KV version 2 secrets with versions, check-and-set, patch, soft delete, undelete and destroy, AppRole sign-in with the token renewed by signing in again, transit encryption, decryption, rewrapping and batches, leases read, renewed and revoked, and Vault's error lists turned into clear errors.//! version: 1.0.0//!//! [dependencies]//! ureq = "2"//! base64 = "0.22"//! serde_json = { version = "1", features = ["preserve_order"] }use base64::Engine;use serde_json::{json, Value};use std::sync::Mutex;use std::time::Duration;// VAULT_API_URL is the server (VAULT_ADDR is read too; http://localhost:8200 when neither is set).// VAULT_TOKEN is a token; or VAULT_ROLE_ID with VAULT_SECRET_ID sign in by AppRole (VAULT_APPROLE_MOUNT// if it is not at approle). VAULT_NAMESPACE names a namespace, VAULT_KV_MOUNT the KV engine (secret// when not set), VAULT_TRANSIT_MOUNT the transit engine (transit when not set).fn env(k: &str) -> Option<String> {    std::env::var(k).ok().filter(|v| !v.is_empty())}fn base() -> String {    env("VAULT_API_URL").or(env("VAULT_ADDR")).unwrap_or_else(|| "http://localhost:8200".into()).trim_end_matches('/').to_string()}fn now() -> i64 {    std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs() as i64)}fn enc(s: &str) -> String {    s.bytes().map(|b| if b.is_ascii_alphanumeric() || b"-_.~".contains(&b) { (b as char).to_string() } else { format!("%{:02X}", b) }).collect()}fn enc_path(p: &str) -> Result<String, String> {    let t = p.trim_matches('/');    if t.is_empty() {        return Err("no path given".into());    }    if t.split('/').any(|x| x == ".." || x == ".") {        return Err(format!("{:?}: a path has no . or .. in it", p));    }    Ok(t.split('/').map(enc).collect::<Vec<_>>().join("/"))}static LOGIN: Mutex<Option<(String, i64)>> = Mutex::new(None);struct Reply {    status: u16,    body: Value,    text: String,}fn raw(method: &str, path: &str, token: Option<&str>, body: Option<(&str, String)>) -> Result<Reply, String> {    let mut req = ureq::request(method, &format!("{}/v1/{}", base(), path)).timeout(Duration::from_secs(30)).set("X-Vault-Request", "true");    if let Some(t) = token {        req = req.set("X-Vault-Token", t);    }    if let Some(n) = env("VAULT_NAMESPACE") {        req = req.set("X-Vault-Namespace", &n);    }    let res = match body {        Some((kind, b)) => req.set("Content-Type", kind).send_string(&b),        None => req.call(),    };    let (status, text) = match res {        Ok(r) => (r.status(), r.into_string().map_err(|e| e.to_string())?),        Err(ureq::Error::Status(c, r)) => (c, r.into_string().unwrap_or_default()),        Err(e) => return Err(format!("cannot reach Vault at {}: {}", base(), e)),    };    let body = serde_json::from_str(&text).unwrap_or(Value::Null);    Ok(Reply { status, body, text })}fn refused(r: &Reply) -> String {    let list: Vec<String> = r.body["errors"].as_array().into_iter().flatten().filter_map(|e| e.as_str().map(str::to_string)).collect();    let what = if list.is_empty() { r.text.trim().chars().take(200).collect() } else { list.join("; ") };    let hint = match r.status {        403 => " (check the token and its policy)",        503 => " (Vault is sealed, or not ready)",        _ => "",    };    format!("Vault answered {}: {}{}", r.status, what, hint)}/// A token from AppRole: role and secret id traded for one, kept until five minutes before it ends.fn approle() -> Result<String, String> {    let (Some(role), Some(secret)) = (env("VAULT_ROLE_ID"), env("VAULT_SECRET_ID")) else {        return Err("set VAULT_TOKEN, or VAULT_ROLE_ID with VAULT_SECRET_ID".into());    };    let mount = env("VAULT_APPROLE_MOUNT").unwrap_or_else(|| "approle".into());    let r = raw("POST", &format!("auth/{}/login", mount), None, Some(("application/json", json!({ "role_id": role, "secret_id": secret }).to_string())))?;    if r.status >= 300 {        return Err(format!("the AppRole sign-in failed: {}", refused(&r)));    }    let t = r.body["auth"]["client_token"].as_str().ok_or("Vault gave no client_token")?.to_string();    let ttl = r.body["auth"]["lease_duration"].as_i64().unwrap_or(0);    *LOGIN.lock().unwrap_or_else(|e| e.into_inner()) = Some((t.clone(), if ttl > 0 { now() + ttl } else { i64::MAX }));    Ok(t)}fn token() -> Result<String, String> {    if let Some(t) = env("VAULT_TOKEN") {        return Ok(t);    }    if let Some((t, until)) = LOGIN.lock().unwrap_or_else(|e| e.into_inner()).as_ref() {        if *until > now() + 300 {            return Ok(t.clone());        }    }    approle()}/// With an AppRole token a 403 can mean it was revoked or ended early: one sign-in again.fn send(method: &str, path: &str, body: Option<(&str, String)>) -> Result<Reply, String> {    let r = raw(method, path, Some(&token()?), body.clone())?;    if r.status == 403 && env("VAULT_TOKEN").is_none() {        *LOGIN.lock().unwrap_or_else(|e| e.into_inner()) = None;        return raw(method, path, Some(&approle()?), body);    }    Ok(r)}fn call(method: &str, path: &str, body: Option<&Value>) -> Result<Value, String> {    let r = send(method, path, body.map(|b| ("application/json", b.to_string())))?;    if r.status >= 300 {        return Err(refused(&r));    }    Ok(r.body)}fn mount() -> String {    env("VAULT_KV_MOUNT").unwrap_or_else(|| "secret".into())}fn transit() -> String {    env("VAULT_TRANSIT_MOUNT").unwrap_or_else(|| "transit".into())}/// Signs in by AppRole now, from VAULT_ROLE_ID and VAULT_SECRET_ID, and gives `{ ttl_seconds }`. Calls do it themselves when they need to; this is for finding a bad secret id at start.pub fn vault_login() -> Result<Value, String> {    approle()?;    let until = LOGIN.lock().unwrap_or_else(|e| e.into_inner()).as_ref().map_or(0, |x| x.1);    Ok(json!({ "ttl_seconds": if until == i64::MAX { 0 } else { until - now() } }))}/// Vault's state from /sys/health, which answers by status code: `{ initialized, sealed, standby, version }`, a standby or a sealed server being an answer, not an error.pub fn vault_health() -> Result<Value, String> {    let r = raw("GET", "sys/health", None, None)?;    if !r.body.is_object() {        return Err(format!("Vault answered {}: {}", r.status, r.text.chars().take(200).collect::<String>()));    }    Ok(json!({ "status": r.status, "initialized": r.body["initialized"], "sealed": r.body["sealed"], "standby": r.body["standby"], "version": r.body["version"] }))}/// Reads a KV v2 secret; `version` 0 for the newest. Gives `{ data, version, created_time, deleted_time, destroyed }`, `data` null for a version deleted or destroyed, or null when there is no such secret.pub fn vault_kv_get(path: &str, version: i64) -> Result<Value, String> {    let q = if version > 0 { format!("?version={}", version) } else { String::new() };    let r = send("GET", &format!("{}/data/{}{}", mount(), enc_path(path)?, q), None)?;    // A deleted version is a 404 that still carries its metadata.    if r.status == 404 {        if r.body["data"]["metadata"].is_object() {            let m = &r.body["data"]["metadata"];            return Ok(json!({ "data": null, "version": m["version"], "created_time": m["created_time"], "deleted_time": m["deletion_time"], "destroyed": m["destroyed"] }));        }        return Ok(Value::Null);    }    if r.status >= 300 {        return Err(refused(&r));    }    let (d, m) = (&r.body["data"]["data"], &r.body["data"]["metadata"]);    Ok(json!({ "data": d, "version": m["version"], "created_time": m["created_time"], "deleted_time": null, "destroyed": m["destroyed"] }))}/// Writes a new version of a secret (a map). `cas` is -1 for no check, 0 to write only if there is none, or the version it must still be at. Gives `{ version, created_time }`. A failed check is an error saying so.pub fn vault_kv_put(path: &str, data: serde_json::Value, cas: i64) -> Result<Value, String> {    if !data.is_object() {        return Err("a secret is a map".into());    }    let mut b = json!({ "data": data });    if cas >= 0 {        b["options"] = json!({ "cas": cas });    }    let r = call("POST", &format!("{}/data/{}", mount(), enc_path(path)?), Some(&b))?;    Ok(json!({ "version": r["data"]["version"], "created_time": r["data"]["created_time"] }))}/// Merges fields into the newest version as a new version, others staying: JSON merge patch, a field given as null being removed. Needs the `patch` capability. Gives `{ version, created_time }`.pub fn vault_kv_patch(path: &str, changes: serde_json::Value, cas: i64) -> Result<Value, String> {    let mut b = json!({ "data": changes });    if cas >= 0 {        b["options"] = json!({ "cas": cas });    }    let r = send("PATCH", &format!("{}/data/{}", mount(), enc_path(path)?), Some(("application/merge-patch+json", b.to_string())))?;    if r.status >= 300 {        return Err(refused(&r));    }    Ok(json!({ "version": r.body["data"]["version"], "created_time": r.body["data"]["created_time"] }))}fn versions_of(versions: &Value) -> Result<Value, String> {    if !versions.as_array().map_or(false, |a| !a.is_empty() && a.iter().all(|v| v.is_i64())) {        return Err("versions are a list of numbers".into());    }    Ok(json!({ "versions": versions }))}/// Soft-deletes versions (a list), or the newest when `versions` is null. They can come back with `vault_kv_undelete`.pub fn vault_kv_delete(path: &str, versions: serde_json::Value) -> Result<bool, String> {    let p = enc_path(path)?;    if versions.is_null() {        call("DELETE", &format!("{}/data/{}", mount(), p), None)?;    } else {        call("POST", &format!("{}/delete/{}", mount(), p), Some(&versions_of(&versions)?))?;    }    Ok(true)}/// Brings soft-deleted versions back.pub fn vault_kv_undelete(path: &str, versions: serde_json::Value) -> Result<bool, String> {    call("POST", &format!("{}/undelete/{}", mount(), enc_path(path)?), Some(&versions_of(&versions)?))?;    Ok(true)}/// Destroys versions for good: their data cannot come back.pub fn vault_kv_destroy(path: &str, versions: serde_json::Value) -> Result<bool, String> {    call("POST", &format!("{}/destroy/{}", mount(), enc_path(path)?), Some(&versions_of(&versions)?))?;    Ok(true)}/// A secret's metadata: `{ current_version, oldest_version, versions: { "1": { created_time, deletion_time, destroyed } }, ... }`, or null.pub fn vault_kv_metadata(path: &str) -> Result<Value, String> {    let r = send("GET", &format!("{}/metadata/{}", mount(), enc_path(path)?), None)?;    if r.status == 404 {        return Ok(Value::Null);    }    if r.status >= 300 {        return Err(refused(&r));    }    Ok(r.body["data"].clone())}/// The names under a folder of secrets, folders ending in /. None is an empty list.pub fn vault_kv_list(path: &str) -> Result<Value, String> {    let p = path.trim_matches('/');    let at = if p.is_empty() { format!("{}/metadata", mount()) } else { format!("{}/metadata/{}", mount(), enc_path(p)?) };    // GET with list=true is the same call as Vault's LIST method, and passes through proxies.    let r = send("GET", &format!("{}?list=true", at), None)?;    if r.status == 404 {        return Ok(json!([]));    }    if r.status >= 300 {        return Err(refused(&r));    }    Ok(r.body["data"]["keys"].clone())}fn b64(s: &str) -> String {    base64::engine::general_purpose::STANDARD.encode(s.as_bytes())}fn unb64(s: &str) -> Result<String, String> {    let b = base64::engine::general_purpose::STANDARD.decode(s).map_err(|e| format!("Vault's plaintext is not base64: {}", e))?;    Ok(String::from_utf8_lossy(&b).into_owned())}/// Encrypts text with a transit key, which stays in Vault. `context` is "" or the derivation context of a derived key. Gives `vault:v1:...`, to keep in a database.pub fn vault_encrypt(key: &str, plaintext: &str, context: &str) -> Result<String, String> {    let mut b = json!({ "plaintext": b64(plaintext) });    if !context.is_empty() {        b["context"] = json!(b64(context));    }    let r = call("POST", &format!("{}/encrypt/{}", transit(), enc(key)), Some(&b))?;    r["data"]["ciphertext"].as_str().map(str::to_string).ok_or("Vault gave no ciphertext".to_string())}/// Decrypts what `vault_encrypt` gave, back to text.pub fn vault_decrypt(key: &str, ciphertext: &str, context: &str) -> Result<String, String> {    if !ciphertext.starts_with("vault:v") {        return Err("that is not Vault ciphertext: it starts vault:v".into());    }    let mut b = json!({ "ciphertext": ciphertext });    if !context.is_empty() {        b["context"] = json!(b64(context));    }    let r = call("POST", &format!("{}/decrypt/{}", transit(), enc(key)), Some(&b))?;    unb64(r["data"]["plaintext"].as_str().ok_or("Vault gave no plaintext")?)}/// Re-encrypts ciphertext under the key's newest version, without showing the plaintext to anyone: for after `vault_rotate`.pub fn vault_rewrap(key: &str, ciphertext: &str) -> Result<String, String> {    let r = call("POST", &format!("{}/rewrap/{}", transit(), enc(key)), Some(&json!({ "ciphertext": ciphertext })))?;    r["data"]["ciphertext"].as_str().map(str::to_string).ok_or("Vault gave no ciphertext".to_string())}/// Encrypts many texts in one call. One bad item does not fail the rest: gives a list in order, each `{ ciphertext }` or `{ error }`.pub fn vault_encrypt_many(key: &str, texts: Vec<String>) -> Result<Value, String> {    let items: Vec<Value> = texts.iter().map(|t| json!({ "plaintext": b64(t) })).collect();    let r = call("POST", &format!("{}/encrypt/{}", transit(), enc(key)), Some(&json!({ "batch_input": items })))?;    Ok(r["data"]["batch_results"].clone())}/// Decrypts many ciphertexts in one call: a list in order, each `{ text }` or `{ error }`.pub fn vault_decrypt_many(key: &str, ciphertexts: Vec<String>) -> Result<Vec<Value>, String> {    let items: Vec<Value> = ciphertexts.iter().map(|t| json!({ "ciphertext": t })).collect();    let r = call("POST", &format!("{}/decrypt/{}", transit(), enc(key)), Some(&json!({ "batch_input": items })))?;    Ok(r["data"]["batch_results"].as_array().into_iter().flatten().map(|x| {        match (x["error"].as_str().filter(|e| !e.is_empty()), x["plaintext"].as_str()) {            (Some(e), _) => json!({ "error": e }),            (None, Some(p)) => unb64(p).map_or_else(|e| json!({ "error": e }), |t| json!({ "text": t })),            _ => json!({ "error": "no plaintext" }),        }    }).collect())}/// Adds a new version to a transit key: new encryptions use it, and old ciphertext still decrypts.pub fn vault_rotate(key: &str) -> Result<bool, String> {    call("POST", &format!("{}/keys/{}/rotate", transit(), enc(key)), Some(&json!({})))?;    Ok(true)}/// Reads any path, such as `database/creds/readonly` for dynamic credentials: `{ data, lease_id, lease_duration, renewable, warnings }`, or null on a 404.pub fn vault_read(path: &str) -> Result<Value, String> {    let r = send("GET", &enc_path(path)?, None)?;    if r.status == 404 {        return Ok(Value::Null);    }    if r.status >= 300 {        return Err(refused(&r));    }    let b = r.body;    Ok(json!({ "data": b["data"], "lease_id": b["lease_id"], "lease_duration": b["lease_duration"], "renewable": b["renewable"], "warnings": b["warnings"] }))}/// Writes to any path with a map, for what this plugin has no function for; gives Vault's answer.pub fn vault_write(path: &str, data: serde_json::Value) -> Result<Value, String> {    call("POST", &enc_path(path)?, Some(&data))}/// Renews a lease by `increment_seconds` (0 for its default). Gives `{ lease_id, lease_duration, renewable }`: Vault may give less than asked, near its maximum.pub fn vault_lease_renew(lease_id: &str, increment_seconds: i64) -> Result<Value, String> {    let mut b = json!({ "lease_id": lease_id });    if increment_seconds > 0 {        b["increment"] = json!(increment_seconds);    }    let r = call("PUT", "sys/leases/renew", Some(&b))?;    Ok(json!({ "lease_id": r["lease_id"], "lease_duration": r["lease_duration"], "renewable": r["renewable"] }))}/// What a lease has left: `{ id, ttl, expire_time, renewable }`.pub fn vault_lease_lookup(lease_id: &str) -> Result<Value, String> {    let r = call("PUT", "sys/leases/lookup", Some(&json!({ "lease_id": lease_id })))?;    let d = &r["data"];    Ok(json!({ "id": d["id"], "ttl": d["ttl"], "expire_time": d["expire_time"], "renewable": d["renewable"] }))}/// Ends a lease now, and with it what it stands for, a database user say.pub fn vault_lease_revoke(lease_id: &str) -> Result<bool, String> {    call("PUT", "sys/leases/revoke", Some(&json!({ "lease_id": lease_id })))?;    Ok(true)}
Thread
47Useful? Like it, or add to its thread below.

Thread 0 posts

On HashiCorp Vault: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.