use crate::requirements::SecretFormat;
use crate::types::Scope;
use crate::uri::SecretUri;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GeneratedSecretScope {
pub level: String,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub team: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GeneratedSecretRequirement {
pub policy: String,
pub length: usize,
pub encoding: String,
pub scope: GeneratedSecretScope,
pub regenerate_if_present: bool,
}
pub fn generated_scope_team<'a>(
generated: &'a GeneratedSecretRequirement,
default_team: Option<&'a str>,
) -> Option<&'a str> {
if generated.scope.level.eq_ignore_ascii_case("tenant")
|| generated.scope.team.as_deref() == Some("_")
{
return None;
}
generated.scope.team.as_deref().or(default_team)
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct PackSecretRequirement {
pub key: String,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Vec::is_empty")
)]
pub aliases: Vec<String>,
pub required: bool,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub generated: Option<GeneratedSecretRequirement>,
}
impl PackSecretRequirement {
pub fn user_supplied(key: impl Into<String>) -> Self {
Self {
key: key.into(),
aliases: Vec::new(),
required: true,
generated: None,
}
}
pub fn generated(key: impl Into<String>, generated: GeneratedSecretRequirement) -> Self {
Self {
key: key.into(),
aliases: Vec::new(),
required: true,
generated: Some(generated),
}
}
pub fn is_generated(&self) -> bool {
self.generated.is_some()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum SecretSource {
UserSupplied,
Generated(GeneratedSecretRequirement),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ManagedSecret {
pub uri: SecretUri,
pub required: bool,
pub source: SecretSource,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub format: Option<SecretFormat>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub description: Option<String>,
}
impl ManagedSecret {
pub fn user_supplied(uri: SecretUri) -> Self {
Self {
uri,
required: true,
source: SecretSource::UserSupplied,
format: None,
description: None,
}
}
pub fn generated(uri: SecretUri, generated: GeneratedSecretRequirement) -> Self {
Self {
uri,
required: true,
source: SecretSource::Generated(generated),
format: None,
description: None,
}
}
pub fn is_generated(&self) -> bool {
matches!(self.source, SecretSource::Generated(_))
}
pub fn generated_requirement(&self) -> Option<&GeneratedSecretRequirement> {
match &self.source {
SecretSource::Generated(generated) => Some(generated),
SecretSource::UserSupplied => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SecretSet {
pub scope: Scope,
pub secrets: Vec<ManagedSecret>,
}
impl SecretSet {
pub fn new(scope: Scope) -> Self {
Self {
scope,
secrets: Vec::new(),
}
}
pub fn push(&mut self, secret: ManagedSecret) {
self.secrets.push(secret);
}
pub fn generated(&self) -> impl Iterator<Item = &ManagedSecret> {
self.secrets.iter().filter(|secret| secret.is_generated())
}
pub fn user_supplied(&self) -> impl Iterator<Item = &ManagedSecret> {
self.secrets.iter().filter(|secret| !secret.is_generated())
}
}
#[cfg(all(test, feature = "serde"))]
mod tests {
use super::*;
fn raw_text(length: usize, level: &str, team: Option<&str>) -> GeneratedSecretRequirement {
GeneratedSecretRequirement {
policy: "random".to_string(),
length,
encoding: "raw_text".to_string(),
scope: GeneratedSecretScope {
level: level.to_string(),
team: team.map(str::to_string),
},
regenerate_if_present: false,
}
}
#[test]
fn generated_requirement_serde_round_trips() {
let g = raw_text(20, "tenant", Some("_"));
let json = serde_json::to_string(&g).unwrap();
let back: GeneratedSecretRequirement = serde_json::from_str(&json).unwrap();
assert_eq!(back, g);
}
#[test]
fn generated_scope_team_collapses_tenant_and_underscore() {
assert_eq!(
generated_scope_team(&raw_text(20, "tenant", None), Some("legal")),
None
);
assert_eq!(
generated_scope_team(&raw_text(20, "tenant", Some("legal")), Some("ops")),
None
);
assert_eq!(
generated_scope_team(&raw_text(20, "team", Some("_")), Some("legal")),
None
);
assert_eq!(
generated_scope_team(&raw_text(20, "team", Some("legal")), Some("ops")),
Some("legal")
);
assert_eq!(
generated_scope_team(&raw_text(20, "team", None), Some("ops")),
Some("ops")
);
}
#[test]
fn managed_secret_partitions_by_source() {
let scope = Scope::new("dev", "demo", None).unwrap();
let mut set = SecretSet::new(scope);
set.push(ManagedSecret::user_supplied(
SecretUri::parse("secrets://dev/demo/_/messaging-slack/api_key").unwrap(),
));
set.push(ManagedSecret::generated(
SecretUri::parse("secrets://dev/demo/_/messaging-telegram/webhook_secret").unwrap(),
raw_text(32, "tenant", Some("_")),
));
assert_eq!(set.generated().count(), 1);
assert_eq!(set.user_supplied().count(), 1);
}
}