Infrastructure

AWS Secrets and Parameters

Read Secrets Manager secrets by version or stage, open JSON ones, see where a rotation stands, read Parameter Store values one by one or by path with their history, and keep them a short while so a page view is not an API call.

Version 1.0.0 · 12 functions · 5 crates

#aws#secrets#configuration#signing

Functions

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

secrets_forget() → int

Empties the cache, after a rotation say. Gives how many values it held.

secrets_get(id: str, options: any) → anyor an error

A secret: { name, arn, value, binary, version_id, stages, created }. value is its text; binary its base64 when it holds bytes. Options: version_id, stage (AWSCURRENT by default; AWSPREVIOUS for the one before), cache_ms (30 s; 0 asks every time).

secrets_json(id: str, options: any) → anyor an error

A secret holding JSON, opened: secrets_json("db")?.password. An error value when it is not JSON, without showing the text.

secrets_versions(id: str, options: any) → listor an error

A secret's versions: { version_id, stages, created, last_accessed }, every page.

secrets_rotation(id: str, options: any) → anyor an error

Where a secret's rotation stands: { enabled, state, every_days, last_rotated, next_rotation, current, pending, previous }. state is "off", "scheduled", or "rotating" while a version has AWSPENDING and is not yet AWSCURRENT, which is when two passwords both work and a new connection may need either.

secrets_put(id: str, value: any, options: any) → anyor an error

Stores a new value (text, or a map turned into JSON), which becomes AWSCURRENT unless the option stages says otherwise. Gives { version_id, stages }, and forgets the cache.

secrets_rotate(id: str, options: any) → stror an error

Starts a rotation now with the rotation function already set on the secret. Gives the new version's id.

param_get(name: str, options: any) → anyor an error

A parameter: { name, value, type, version, modified }, SecureStrings decrypted unless the option decrypt is false. name:label or name:3 picks a label or version. A missing one is an error saying ParameterNotFound. Takes cache_ms like a secret.

param_path(path: str, options: any) → listor an error

Every parameter under a path, like /app/prod/, as a list: GetParametersByPath, all pages. Options: recursive (true), decrypt (true).

param_map(path: str, options: any) → anyor an error

The parameters under a path as a map from the name's last part to its value: { host: "...", port: "5432" }, for config.

param_history(name: str, options: any) → listor an error

A parameter's earlier values, oldest first: { version, value, type, modified, modified_by, labels }.

param_put(name: str, value: str, options: any) → intor an error

Writes a parameter. Options: type (String, StringList, SecureString; String by default), overwrite (false: an existing one is an error), description, key_id. Gives its new version, and forgets the cache.

Get it

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

Or with curl:

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

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

main.qwe
use aws_secrets.qweruse secrets_forget, secrets_get from aws_secrets      // Quillo 0.6: only these

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

[dependencies]
ureq = "2"hmac = "0.12"sha2 = "0.10"hex = "0.4"serde_json = { version = "1", features = ["preserve_order"] }

Example

aws_secrets.qwe
// A program's credentials and settings kept in AWS: a database password read from Secrets Manager// (and not asked for again on each request), the password before it, where a rotation stands, and// a site's settings read from Parameter Store by path, with a parameter's history.////   AWS_REGION=eu-west-1 AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... quillo run aws_secrets.qwe//   then open http://localhost:8080/secret, /cache, /rotation, /put, /params and /missing//// It wants a secret prod/db holding JSON, and parameters under /app/prod/. SECRETSMANAGER_API_URL// and SSM_API_URL reach LocalStack or a test server.use "aws_secrets.qwer"route GET /secret {    let s = secrets_get("prod/db", { cache_ms: 0 })?    let old = secrets_get("prod/db", { stage: "AWSPREVIOUS", cache_ms: 0 })?    { name: s.name, stages: s.stages, user: secrets_json("prod/db", {})?.username, port: secrets_json("prod/db", {})?.port, previous: json.parse(old.value)?.password }}// Ten reads, one request: the stand-in counts them, /calls says how many it had.route GET /cache {    secrets_forget()    for i in range(0, 10) { secrets_get("prod/api-key", { cache_ms: 60000 })? }    { value: secrets_get("prod/api-key", {})?.value, calls: http.get(env("SECRETSMANAGER_API_URL", "") + "/_calls").json.calls }}route GET /rotation = {    stable: secrets_rotation("prod/db", {})?,    midway: secrets_rotation("prod/rotating", {})?.state,    never: secrets_rotation("prod/api-key", {})?.state,    versions: len(secrets_versions("prod/db", {})?),}route GET /put {    let put = secrets_put("prod/db", { username: "app", password: "new-one", port: 5432 }, {})?    let read = secrets_json("prod/db", { cache_ms: 0 })?    { stage: put.stages, password: read.password, rotated: len(secrets_rotate("prod/db", {})?) > 0 }}route GET /params {    let all = param_path("/app/prod/", {})?    param_put("/app/prod/banner", "Back at 9", { overwrite: true })?    {        count: len(all),        host: param_get("/app/prod/db/host", { cache_ms: 0 })?.value,        secure: param_get("/app/prod/token", { cache_ms: 0 })?,        shown: param_get("/app/prod/token", { decrypt: false, cache_ms: 0 })?.value != "open-sesame",        config: param_map("/app/prod/", {})?,        history: map(param_history("/app/prod/banner", {})?, (p) => p.value),        taken: param_put("/app/prod/banner", "x", {}).error,    }}route GET /missing = {    secret: secrets_get("prod/nothing", {}).error,    param: param_get("/app/nothing", {}).error,    not_json: secrets_json("prod/api-key", {}).error,}
$ quillo run aws_secrets.qwe$ curl 'localhost:8080/secret'$ curl 'localhost:8080/cache'$ curl 'localhost:8080/rotation'$ curl 'localhost:8080/put'$ curl 'localhost:8080/params'$ curl 'localhost:8080/missing'

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

aws_secrets.qwer
//! AWS Secrets Manager and Systems Manager Parameter Store without an SDK: secrets read by version or stage, JSON secrets opened, rotation state told plainly, parameters read one by one or by path with their history, new values written, and a short in-process cache so a page view is not an API call, all signed with Signature Version 4.//! version: 1.0.0//!//! [dependencies]//! ureq = "2"//! hmac = "0.12"//! sha2 = "0.10"//! hex = "0.4"//! serde_json = { version = "1", features = ["preserve_order"] }use hmac::{Hmac, Mac};use serde_json::{json, Map, Value};use sha2::{Digest, Sha256};use std::collections::HashMap;use std::io::Read;use std::sync::Mutex;use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};// ---- AWS: credentials from the environment, Signature Version 4, one signed HTTP call ----struct Aws {    region: String,    key: String,    secret: String,    token: Option<String>,    base: String,    host: String,    service: String,    timeout: Duration,}struct Reply {    status: u16,    headers: Vec<(String, String)>,    body: Vec<u8>,}impl Reply {    fn header(&self, name: &str) -> Option<&str> {        self.headers.iter().find(|(k, _)| k.eq_ignore_ascii_case(name)).map(|(_, v)| v.as_str())    }    fn text(&self) -> String {        String::from_utf8_lossy(&self.body).into_owned()    }}fn opt_text(o: &Value, k: &str) -> Option<String> {    match o.get(k) {        Some(Value::String(s)) if !s.is_empty() => Some(s.clone()),        Some(Value::Number(n)) => Some(n.to_string()),        _ => None,    }}fn var(name: &str) -> Option<String> {    std::env::var(name).ok().filter(|v| !v.is_empty())}/// The account to call: keys from the environment only, the region from `region` or AWS_REGION, the address from `url_var` when set.fn account(o: &Value, service: &str, url_var: &str, host_prefix: &str) -> Result<Aws, String> {    let key = var("AWS_ACCESS_KEY_ID").ok_or("no AWS key: set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY")?;    let secret = var("AWS_SECRET_ACCESS_KEY").ok_or("no AWS secret: set AWS_SECRET_ACCESS_KEY")?;    let region = opt_text(o, "region").or_else(|| var("AWS_REGION")).or_else(|| var("AWS_DEFAULT_REGION")).ok_or("no region: set AWS_REGION, or give the option region")?;    let base = var(url_var).unwrap_or_else(|| {        let suffix = if region.starts_with("cn-") { "amazonaws.com.cn" } else { "amazonaws.com" };        format!("https://{}.{}.{}", host_prefix, region, suffix)    });    let base = base.trim_end_matches('/').to_string();    let host = base.split("://").nth(1).unwrap_or(&base).split('/').next().unwrap_or("").to_string();    let ms = o.get("timeout_ms").and_then(|v| v.as_u64()).unwrap_or(15_000).clamp(100, 900_000);    Ok(Aws { region, key, secret, token: var("AWS_SESSION_TOKEN"), base, host, service: service.to_string(), timeout: Duration::from_millis(ms) })}fn hmac(key: &[u8], msg: &str) -> Vec<u8> {    let mut m = Hmac::<Sha256>::new_from_slice(key).expect("HMAC takes any key");    m.update(msg.as_bytes());    m.finalize().into_bytes().to_vec()}fn sha_hex(b: &[u8]) -> String {    hex::encode(Sha256::digest(b))}/// RFC 3986 encoding: only A-Z a-z 0-9 - _ . ~ kept (and `/` when asked).fn enc(s: &str, keep_slash: bool) -> String {    s.bytes().map(|b| if b.is_ascii_alphanumeric() || b"-_.~".contains(&b) || (keep_slash && b == b'/') { (b as char).to_string() } else { format!("%{:02X}", b) }).collect()}fn civil(z: i64) -> (i64, i64, i64) {    let z = z + 719468;    let era = z.div_euclid(146097);    let doe = z - era * 146097;    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);    let mp = (5 * doy + 2) / 153;    let d = doy - (153 * mp + 2) / 5 + 1;    let m = if mp < 10 { mp + 3 } else { mp - 9 };    (yoe + era * 400 + if m <= 2 { 1 } else { 0 }, m, d)}fn amz_now() -> String {    let secs = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0);    let (y, m, d) = civil(secs.div_euclid(86400));    let rem = secs.rem_euclid(86400);    format!("{:04}{:02}{:02}T{:02}{:02}{:02}Z", y, m, d, rem / 3600, rem % 3600 / 60, rem % 60)}/// The headers that sign a request. `path` is as sent (already encoded once); all services here but S3 sign it encoded a second time.fn sign(a: &Aws, method: &str, path: &str, query: &str, extra: &[(String, String)], body: &[u8]) -> Vec<(String, String)> {    let stamp = amz_now();    let date = &stamp[..8];    let mut heads: Vec<(String, String)> = vec![("host".into(), a.host.clone()), ("x-amz-date".into(), stamp.clone())];    if let Some(t) = &a.token {        heads.push(("x-amz-security-token".into(), t.clone()));    }    for (k, v) in extra {        heads.push((k.to_lowercase(), v.trim().to_string()));    }    heads.sort();    let names = heads.iter().map(|(k, _)| k.as_str()).collect::<Vec<_>>().join(";");    let canon_heads: String = heads.iter().map(|(k, v)| format!("{}:{}\n", k, v)).collect();    let mut pairs: Vec<&str> = query.split('&').filter(|p| !p.is_empty()).collect();    pairs.sort();    let canon = format!("{}\n{}\n{}\n{}\n{}\n{}", method, enc(path, true), pairs.join("&"), canon_heads, names, sha_hex(body));    let scope = format!("{}/{}/{}/aws4_request", date, a.region, a.service);    let to_sign = format!("AWS4-HMAC-SHA256\n{}\n{}\n{}", stamp, scope, sha_hex(canon.as_bytes()));    let k = hmac(format!("AWS4{}", a.secret).as_bytes(), date);    let k = hmac(&k, &a.region);    let k = hmac(&k, &a.service);    let k = hmac(&k, "aws4_request");    let sig = hex::encode(hmac(&k, &to_sign));    let mut out = extra.to_vec();    out.push(("X-Amz-Date".into(), stamp.clone()));    if let Some(t) = &a.token {        out.push(("X-Amz-Security-Token".into(), t.clone()));    }    out.push(("Authorization".into(), format!("AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}", a.key, scope, names, sig)));    out}/// One signed call. Any status comes back as a Reply; only a network failure is an error.fn send(a: &Aws, method: &str, path: &str, query: &str, extra: &[(String, String)], body: &[u8]) -> Result<Reply, String> {    let url = if query.is_empty() { format!("{}{}", a.base, path) } else { format!("{}{}?{}", a.base, path, query) };    let agent = ureq::AgentBuilder::new().timeout_connect(Duration::from_secs(10)).timeout_read(a.timeout).redirects(0).build();    let mut r = agent.request(method, &url);    for (k, v) in sign(a, method, path, query, extra, body) {        r = r.set(&k, &v);    }    let res = if body.is_empty() && method != "POST" && method != "PUT" { r.call() } else { r.send_bytes(body) };    let resp = match res {        Ok(x) => x,        Err(ureq::Error::Status(_, x)) => x,        Err(e) => return Err(format!("cannot reach AWS at {}: {}", a.base, e)),    };    let status = resp.status();    let headers = resp.headers_names().iter().map(|n| (n.clone(), resp.header(n).unwrap_or("").to_string())).collect();    let mut body = vec![];    resp.into_reader().take(64 << 20).read_to_end(&mut body).map_err(|e| format!("cut off reading AWS's answer: {}", e))?;    Ok(Reply { status, headers, body })}// ---- The JSON call both services take ----fn call(service: &str, target: &str, body: &Value, o: &Value) -> Result<Value, String> {    let (acct, name) = if service == "ssm" { (account(o, "ssm", "SSM_API_URL", "ssm")?, "SSM") } else { (account(o, "secretsmanager", "SECRETSMANAGER_API_URL", "secretsmanager")?, "Secrets Manager") };    let heads = vec![("Content-Type".to_string(), "application/x-amz-json-1.1".to_string()), ("X-Amz-Target".to_string(), target.to_string())];    let text = body.to_string();    let mut n = 0u32;    loop {        n += 1;        let r = send(&acct, "POST", "/", "", &heads, text.as_bytes())?;        if r.status == 200 {            return serde_json::from_slice(&r.body).map_err(|e| format!("{}: not JSON: {}", name, e));        }        let v: Value = serde_json::from_slice(&r.body).unwrap_or(Value::Null);        let kind = v["__type"].as_str().and_then(|t| t.rsplit('#').next()).unwrap_or("").to_string();        if (r.status >= 500 || kind.contains("Throttl") || kind == "TooManyUpdates") && n < 6 {            std::thread::sleep(Duration::from_millis(100 << n));            continue;        }        let msg = v["Message"].as_str().or(v["message"].as_str()).map(str::to_string).unwrap_or_else(|| r.text().chars().take(200).collect());        return Err(format!("{}: {}: {}", name, if kind.is_empty() { r.status.to_string() } else { kind }, msg));    }}// ---- The short cache ----static CACHE: Mutex<Option<HashMap<String, (Instant, Value)>>> = Mutex::new(None);fn cached(key: &str, ms: u64) -> Option<Value> {    if ms == 0 {        return None;    }    let g = CACHE.lock().ok()?;    let (at, v) = g.as_ref()?.get(key)?;    (at.elapsed() < Duration::from_millis(ms)).then(|| v.clone())}fn keep(key: String, v: &Value, ms: u64) {    if ms == 0 {        return;    }    if let Ok(mut g) = CACHE.lock() {        g.get_or_insert_with(HashMap::new).insert(key, (Instant::now(), v.clone()));    }}fn cache_ms(o: &Value) -> u64 {    o.get("cache_ms").and_then(|v| v.as_u64()).unwrap_or(30_000)}/// Empties the cache, after a rotation say. Gives how many values it held.pub fn secrets_forget() -> i64 {    match CACHE.lock() {        Ok(mut g) => g.take().map_or(0, |m| m.len() as i64),        Err(_) => 0,    }}/// Epoch seconds, as these APIs send them, as `2026-11-01T09:00:00Z`.fn iso(v: &Value) -> Value {    let Some(t) = v.as_f64() else { return Value::Null };    let secs = t as i64;    let (y, m, d) = civil(secs.div_euclid(86400));    let r = secs.rem_euclid(86400);    json!(format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, m, d, r / 3600, r % 3600 / 60, r % 60))}// ---- Secrets Manager ----/// A secret: { name, arn, value, binary, version_id, stages, created }. `value` is its text; `binary` its base64 when it holds bytes. Options: version_id, stage (AWSCURRENT by default; AWSPREVIOUS for the one before), cache_ms (30 s; 0 asks every time).pub fn secrets_get(id: &str, options: serde_json::Value) -> Result<serde_json::Value, String> {    let ver = opt_text(&options, "version_id");    let stage = opt_text(&options, "stage");    let key = format!("sm\u{0}{}\u{0}{:?}\u{0}{:?}", id, ver, stage);    let ms = cache_ms(&options);    if let Some(v) = cached(&key, ms) {        return Ok(v);    }    let mut body = Map::new();    body.insert("SecretId".into(), json!(id));    if let Some(v) = ver {        body.insert("VersionId".into(), json!(v));    }    if let Some(s) = stage {        body.insert("VersionStage".into(), json!(s));    }    let r = call("sm", "secretsmanager.GetSecretValue", &Value::Object(body), &options)?;    let out = json!({ "name": r["Name"], "arn": r["ARN"], "value": r["SecretString"], "binary": r["SecretBinary"], "version_id": r["VersionId"], "stages": r["VersionStages"], "created": iso(&r["CreatedDate"]) });    keep(key, &out, ms);    Ok(out)}/// A secret holding JSON, opened: `secrets_json("db")?.password`. An error value when it is not JSON, without showing the text.pub fn secrets_json(id: &str, options: serde_json::Value) -> Result<serde_json::Value, String> {    let s = secrets_get(id, options)?;    let t = s["value"].as_str().ok_or(format!("{} holds no text", id))?;    serde_json::from_str(t).map_err(|_| format!("{} does not hold JSON", id))}/// A secret's versions: { version_id, stages, created, last_accessed }, every page.pub fn secrets_versions(id: &str, options: serde_json::Value) -> Result<Vec<serde_json::Value>, String> {    let mut out = vec![];    let mut token = Value::Null;    loop {        let mut body = json!({ "SecretId": id, "IncludeDeprecated": true, "MaxResults": options.get("page_size").and_then(|v| v.as_u64()).unwrap_or(100) });        if !token.is_null() {            body["NextToken"] = token.clone();        }        let r = call("sm", "secretsmanager.ListSecretVersionIds", &body, &options)?;        for v in r["Versions"].as_array().into_iter().flatten() {            out.push(json!({ "version_id": v["VersionId"], "stages": v["VersionStages"], "created": iso(&v["CreatedDate"]), "last_accessed": iso(&v["LastAccessedDate"]) }));        }        token = r["NextToken"].clone();        if token.is_null() {            return Ok(out);        }    }}/// Where a secret's rotation stands: { enabled, state, every_days, last_rotated, next_rotation, current, pending, previous }. `state` is "off", "scheduled", or "rotating" while a version has AWSPENDING and is not yet AWSCURRENT, which is when two passwords both work and a new connection may need either.pub fn secrets_rotation(id: &str, options: serde_json::Value) -> Result<serde_json::Value, String> {    let r = call("sm", "secretsmanager.DescribeSecret", &json!({ "SecretId": id }), &options)?;    let (mut current, mut pending, mut previous) = (Value::Null, Value::Null, Value::Null);    for (ver, stages) in r["VersionIdsToStages"].as_object().into_iter().flatten() {        let has = |s: &str| stages.as_array().is_some_and(|l| l.iter().any(|x| x == s));        if has("AWSCURRENT") {            current = json!(ver);        }        if has("AWSPREVIOUS") {            previous = json!(ver);        }        if has("AWSPENDING") && !has("AWSCURRENT") {            pending = json!(ver);        }    }    let enabled = r["RotationEnabled"] == true;    let state = if !pending.is_null() { "rotating" } else if enabled { "scheduled" } else { "off" };    Ok(json!({ "enabled": enabled, "state": state, "every_days": r["RotationRules"]["AutomaticallyAfterDays"], "last_rotated": iso(&r["LastRotatedDate"]), "next_rotation": iso(&r["NextRotationDate"]), "current": current, "pending": pending, "previous": previous, "deleted": iso(&r["DeletedDate"]) }))}/// Stores a new value (text, or a map turned into JSON), which becomes AWSCURRENT unless the option stages says otherwise. Gives { version_id, stages }, and forgets the cache.pub fn secrets_put(id: &str, value: serde_json::Value, options: serde_json::Value) -> Result<serde_json::Value, String> {    let text = value.as_str().map(str::to_string).unwrap_or_else(|| value.to_string());    let mut body = json!({ "SecretId": id, "SecretString": text, "ClientRequestToken": token() });    if let Some(s) = options.get("stages") {        body["VersionStages"] = s.clone();    }    let r = call("sm", "secretsmanager.PutSecretValue", &body, &options)?;    secrets_forget();    Ok(json!({ "version_id": r["VersionId"], "stages": r["VersionStages"] }))}/// Starts a rotation now with the rotation function already set on the secret. Gives the new version's id.pub fn secrets_rotate(id: &str, options: serde_json::Value) -> Result<String, String> {    let r = call("sm", "secretsmanager.RotateSecret", &json!({ "SecretId": id, "ClientRequestToken": token() }), &options)?;    secrets_forget();    Ok(r["VersionId"].as_str().unwrap_or("").to_string())}/// An idempotency token as a UUID, which PutSecretValue wants and the SDKs fill in by themselves.fn token() -> String {    let now = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_nanos());    let h = sha_hex(format!("{}-{:?}", now, std::thread::current().id()).as_bytes());    format!("{}-{}-4{}-a{}-{}", &h[..8], &h[8..12], &h[13..16], &h[17..20], &h[20..32])}// ---- Parameter Store ----fn param(p: &Value) -> Value {    json!({ "name": p["Name"], "value": p["Value"], "type": p["Type"], "version": p["Version"], "modified": iso(&p["LastModifiedDate"]), "labels": p["Labels"], "description": p["Description"] })}/// A parameter: { name, value, type, version, modified }, SecureStrings decrypted unless the option decrypt is false. `name:label` or `name:3` picks a label or version. A missing one is an error saying ParameterNotFound. Takes cache_ms like a secret.pub fn param_get(name: &str, options: serde_json::Value) -> Result<serde_json::Value, String> {    let dec = options.get("decrypt") != Some(&Value::Bool(false));    let key = format!("ssm\u{0}{}\u{0}{}", name, dec);    let ms = cache_ms(&options);    if let Some(v) = cached(&key, ms) {        return Ok(v);    }    let r = call("ssm", "AmazonSSM.GetParameter", &json!({ "Name": name, "WithDecryption": dec }), &options)?;    let out = param(&r["Parameter"]);    keep(key, &out, ms);    Ok(out)}/// Every parameter under a path, like /app/prod/, as a list: GetParametersByPath, all pages. Options: recursive (true), decrypt (true).pub fn param_path(path: &str, options: serde_json::Value) -> Result<Vec<serde_json::Value>, String> {    let mut out = vec![];    let mut token = Value::Null;    loop {        let mut body = json!({ "Path": path, "Recursive": options.get("recursive") != Some(&Value::Bool(false)), "WithDecryption": options.get("decrypt") != Some(&Value::Bool(false)), "MaxResults": 10 });        if !token.is_null() {            body["NextToken"] = token.clone();        }        let r = call("ssm", "AmazonSSM.GetParametersByPath", &body, &options)?;        out.extend(r["Parameters"].as_array().into_iter().flatten().map(param));        token = r["NextToken"].clone();        if token.is_null() {            return Ok(out);        }    }}/// The parameters under a path as a map from the name's last part to its value: `{ host: "...", port: "5432" }`, for config.pub fn param_map(path: &str, options: serde_json::Value) -> Result<serde_json::Value, String> {    let mut out = Map::new();    let root = path.trim_end_matches('/');    for p in param_path(path, options)? {        let n = p["name"].as_str().unwrap_or("");        out.insert(n.strip_prefix(root).unwrap_or(n).trim_start_matches('/').to_string(), p["value"].clone());    }    Ok(Value::Object(out))}/// A parameter's earlier values, oldest first: { version, value, type, modified, modified_by, labels }.pub fn param_history(name: &str, options: serde_json::Value) -> Result<Vec<serde_json::Value>, String> {    let mut out = vec![];    let mut token = Value::Null;    loop {        let mut body = json!({ "Name": name, "WithDecryption": options.get("decrypt") != Some(&Value::Bool(false)), "MaxResults": 50 });        if !token.is_null() {            body["NextToken"] = token.clone();        }        let r = call("ssm", "AmazonSSM.GetParameterHistory", &body, &options)?;        for p in r["Parameters"].as_array().into_iter().flatten() {            out.push(json!({ "version": p["Version"], "value": p["Value"], "type": p["Type"], "modified": iso(&p["LastModifiedDate"]), "modified_by": p["LastModifiedUser"], "labels": p["Labels"] }));        }        token = r["NextToken"].clone();        if token.is_null() {            return Ok(out);        }    }}/// Writes a parameter. Options: type (String, StringList, SecureString; String by default), overwrite (false: an existing one is an error), description, key_id. Gives its new version, and forgets the cache.pub fn param_put(name: &str, value: &str, options: serde_json::Value) -> Result<i64, String> {    let mut body = json!({ "Name": name, "Value": value, "Type": opt_text(&options, "type").unwrap_or("String".into()), "Overwrite": options.get("overwrite") == Some(&Value::Bool(true)) });    for (k, f) in [("description", "Description"), ("key_id", "KeyId")] {        if let Some(v) = opt_text(&options, k) {            body[f] = json!(v);        }    }    let r = call("ssm", "AmazonSSM.PutParameter", &body, &options)?;    secrets_forget();    Ok(r["Version"].as_i64().unwrap_or(0))}
Amazon DynamoDB aws_dynamodb.qwer · 1.0.0Get, put, update, delete and query DynamoDB with plain maps turned into attribute values and back, pages followed, batch writes with UnprocessedItems sent again, throttling retried and each answer's CRC32 checked, signed with Signature V4.awsdatabaseapisigning2 views · 7 downloadsAmazon SES aws_ses.qwer · 1.0.0Send mail through SES v2, plain, as raw MIME built here with attachments, or from templates in bulk, keep and render templates, read the account's limits and suppression list, and turn the bounce, complaint and delivery notices from SNS into addresses to act on.awsemailnotificationssigning4 views · 9 downloadsAmazon SNS aws_sns.qwer · 1.0.0Publish to SNS with attributes and in batches of ten with failures named, manage topics and subscriptions, and take incoming messages only after checking their RSA signature against SNS's certificate on SNS's own host, SubscriptionConfirmation handled.awsnotificationswebhookssigning4 views · 5 downloadsAWS Lambda aws_lambda.qwer · 1.0.0Invoke Lambda functions and wait, or queue them to run later, with function errors read from their header and the log tail's REPORT line turned into numbers, throttling waited out as Retry-After says, and functions listed and waited on, signed with Signature V4.awsserverlessapisigning3 views · 8 downloadsAWS Signature V4 aws_sign.qwer · 1.0.0AWS Signature V4 for any AWS API: the headers that sign a request for SES, SQS, DynamoDB or S3, and presigned URLs.awsapisignings3email10 views · 12 downloadsHashiCorp Vault vault.qwer · 1.0.0HashiCorp 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.secretsencryptionkmsleases4 views · 7 downloads
Thread
39Useful? Like it, or add to its thread below.

Thread 0 posts

On AWS Secrets and Parameters: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.