use std::path::Path;
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::{Value, json};
use trusty_secrets::api::methods::{ListResponse, ScopeKind, ScopesResponse, method};
use trusty_secrets::server::{DOCTOR, DoctorResponse, OnDemandSecrets};
use trusty_secrets::{BackendId, SecretKey, SecretRef, VaultName};
use super::{describe, with_project};
const TOOL: &str = "secrets_get_ref";
#[derive(Debug, thiserror::Error)]
pub enum GetRefError {
#[error("secrets_get_ref: `project` must be an absolute path to the project directory")]
Project,
#[error("secrets_get_ref: {0}")]
Key(trusty_secrets::SecretsError),
#[error("{0}")]
Client(String),
#[error("secrets_get_ref: {method}: the answer did not decode")]
Decode {
method: &'static str,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RefAnswer {
pub reference: String,
pub key: SecretKey,
pub present: bool,
pub scope: Option<ScopeKind>,
pub vault: Option<VaultName>,
pub backend: BackendId,
pub imported_at: Option<u64>,
}
fn parse_reference(raw: &str) -> Result<SecretRef, GetRefError> {
if raw.starts_with(trusty_secrets::api::SCHEME) {
return SecretRef::parse(raw).map_err(GetRefError::Key);
}
let key = SecretKey::new(raw).map_err(GetRefError::Key)?;
Ok(SecretRef::Unscoped { key })
}
async fn call<T: DeserializeOwned>(
client: &OnDemandSecrets,
project: &Path,
method: &'static str,
params: Value,
) -> Result<T, GetRefError> {
let params = with_project(project, params).ok_or(GetRefError::Project)?;
let answer = client
.call(method, params)
.await
.map_err(|e| GetRefError::Client(describe(TOOL, &e, client.socket())))?;
serde_json::from_value(answer).map_err(|_| GetRefError::Decode { method })
}
pub async fn get_ref(
client: &OnDemandSecrets,
project: &Path,
raw: &str,
) -> Result<RefAnswer, GetRefError> {
if !project.is_absolute() || project.to_str().is_none() {
return Err(GetRefError::Project);
}
let reference = parse_reference(raw)?;
let scopes: ScopesResponse = call(client, project, method::SCOPES, Value::Null).await?;
let candidates: Vec<(ScopeKind, VaultName)> = match reference.pinned_vault() {
Some(vault) => {
let kind = scopes
.scopes
.iter()
.find(|scope| scope.vault == vault)
.map_or(ScopeKind::Owner, |scope| scope.kind);
vec![(kind, vault)]
}
None => scopes
.scopes
.into_iter()
.map(|s| (s.kind, s.vault))
.collect(),
};
let mut hit = None;
for (kind, vault) in candidates {
let listed: ListResponse =
call(client, project, method::LIST, json!({ "vault": vault })).await?;
if let Some(meta) = listed.keys.into_iter().find(|m| &m.name == reference.key()) {
hit = Some((kind, vault, meta.updated_at));
break;
}
}
let doctor: DoctorResponse = call(client, project, DOCTOR, Value::Null).await?;
let (scope, vault, imported_at) = match hit {
Some((kind, vault, at)) => (Some(kind), Some(vault), Some(at)),
None => (None, None, None),
};
tracing::debug!(
reference = %reference,
present = scope.is_some(),
backend = %doctor.selected_backend,
"secrets_get_ref answered"
);
Ok(RefAnswer {
reference: reference.to_string(),
key: reference.key().clone(),
present: scope.is_some(),
scope,
vault,
backend: doctor.selected_backend,
imported_at,
})
}