//! The Flow secrets panel (hub spec ยง13.2, oxygenhub#107) shared by the //! administration, organisation and repository settings pages: names //! only, values typed once and never shown. use crate::auth::Viewer; use crate::db::accounts; use crate::levels::At; use crate::secrets::{self, Seen}; use crate::Shared; use serde::Deserialize; #[derive(Deserialize)] pub struct SecretForm { #[serde(rename = "_csrf")] pub csrf_token: String, #[serde(default)] action: String, #[serde(default)] name: String, #[serde(default)] value: String, } pub struct Panel { pub own: Vec, pub inherited: Vec, } pub async fn panel(app: &Shared, at: &At) -> sqlx::Result { let (own, inherited): (Vec<_>, Vec<_>) = secrets::seen(&app.db, at.scope, &at.owner).await?.into_iter().partition(|v| v.from.is_empty()); Ok(Panel { own, inherited }) } /// Apply the panel's form: `Ok(notice)` or `Err(message for the user)`. pub async fn apply(app: &Shared, viewer: &Viewer, at: &At, f: SecretForm) -> sqlx::Result> { let name = f.name.trim(); if !secrets::name_ok(name) { return Ok(Err("A secret's name is 1 to 64 of A-Z a-z 0-9 . _ -, as the pipeline's `secret` block names it.".into())); } Ok(Ok(match f.action.as_str() { "set" => { let value = f.value.replace("\r\n", "\n"); if value.is_empty() { return Ok(Err("Type the secret's value.".into())); } if value.len() > secrets::MAX_VALUE { return Ok(Err(format!("A value is up to {} bytes.", secrets::MAX_VALUE))); } let own = secrets::list(&app.db, at.scope).await?; if own.len() >= secrets::MAX_PER_LEVEL && !own.iter().any(|s| s.name == name) { return Ok(Err(format!("Up to {} secrets here.", secrets::MAX_PER_LEVEL))); } secrets::set(&app.db, at.scope, name, &app.secret_key.seal(&value)).await?; accounts::audit(&app.db, viewer.account(), "flow.secret.set", &at.label, name).await.ok(); format!("{name} set.") } "delete" => { if !secrets::delete(&app.db, at.scope, name).await? { return Ok(Err(format!("There is no secret {name} here."))); } accounts::audit(&app.db, viewer.account(), "flow.secret.deleted", &at.label, name).await.ok(); format!("{name} removed.") } _ => return Ok(Err("Unknown action.".into())), })) }