use serde::{Deserialize, Serialize};
use crate::envelope::CardKind;
use crate::ids::{CardName, CardUid};
pub const WORKLOAD_UID_HEADER: &str = "x-wyrd-workload-uid";
pub const DEPLOYMENT_ENVIRONMENT_HEADER: &str = "x-wyrd-deployment-environment";
pub fn workload_uid_selector<'a>(
values: impl IntoIterator<Item = &'a [u8]>,
) -> Result<Option<CardUid>, crate::error::WyrdError> {
let refused = || crate::error::WyrdError::WorkloadUidRefused {
message: format!("{WORKLOAD_UID_HEADER} must be sent at most once as one Card UID"),
details: serde_json::json!({ "header": WORKLOAD_UID_HEADER }),
};
let mut values = values.into_iter();
let Some(value) = values.next() else {
return Ok(None);
};
if values.next().is_some() {
return Err(refused());
}
std::str::from_utf8(value)
.ok()
.and_then(|value| CardUid::new(value).ok())
.map(Some)
.ok_or_else(refused)
}
pub fn deployment_environment_selector<'a>(
values: impl IntoIterator<Item = &'a [u8]>,
) -> Result<Option<String>, crate::error::WyrdError> {
let refused = || crate::error::WyrdError::Validation {
message: format!(
"{DEPLOYMENT_ENVIRONMENT_HEADER} must be sent at most once as one non-empty UTF-8 value"
),
details: serde_json::json!({ "header": DEPLOYMENT_ENVIRONMENT_HEADER }),
};
let mut values = values.into_iter();
let Some(value) = values.next() else {
return Ok(None);
};
if values.next().is_some() {
return Err(refused());
}
std::str::from_utf8(value)
.ok()
.filter(|value| !value.is_empty())
.map(|value| Some(value.to_owned()))
.ok_or_else(refused)
}
pub const MAX_SCOPED_CARDS: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum WorkloadKind {
Service,
}
impl WorkloadKind {
#[must_use]
pub const fn card_kind(self) -> CardKind {
match self {
Self::Service => CardKind::Service,
}
}
#[must_use]
pub const fn from_card_kind(kind: &CardKind) -> Option<Self> {
match kind {
CardKind::Service => Some(Self::Service),
_ => None,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Service => "Service",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct WorkloadFamily {
pub kind: WorkloadKind,
pub name: CardName,
}
impl WorkloadFamily {
#[must_use]
pub fn contains(&self, kind: &CardKind, name: &CardName) -> bool {
self.kind.card_kind() == *kind && self.name == *name
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(tag = "mode", rename_all = "snake_case")]
pub enum WorkloadRestriction {
Unrestricted,
Family(WorkloadFamily),
}
impl WorkloadRestriction {
#[must_use]
pub const fn family(&self) -> Option<&WorkloadFamily> {
match self {
Self::Unrestricted => None,
Self::Family(family) => Some(family),
}
}
#[must_use]
pub fn allows(&self, kind: &CardKind, name: &CardName) -> bool {
self.family()
.is_none_or(|family| family.contains(kind, name))
}
}
impl From<Option<WorkloadFamily>> for WorkloadRestriction {
fn from(family: Option<WorkloadFamily>) -> Self {
family.map_or(Self::Unrestricted, Self::Family)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(tag = "mode", rename_all = "snake_case")]
pub enum FamilyRestriction {
Family(WorkloadFamily),
}
impl FamilyRestriction {
#[must_use]
pub const fn family(&self) -> &WorkloadFamily {
let Self::Family(family) = self;
family
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
pub enum CardAccess {
AnyRegistered,
RootRequired {
restriction: FamilyRestriction,
},
Scoped {
cards: Vec<CardUid>,
},
}
impl CardAccess {
#[must_use]
pub fn intersection(&self, other: &Self) -> Option<Self> {
match (self, other) {
(Self::RootRequired { .. }, _) | (_, Self::RootRequired { .. }) => None,
(Self::AnyRegistered, any) | (any, Self::AnyRegistered) => Some(any.clone()),
(Self::Scoped { cards }, Self::Scoped { cards: theirs }) => Some(Self::Scoped {
cards: cards
.iter()
.filter(|card| theirs.contains(card))
.cloned()
.collect(),
}),
}
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{CardAccess, FamilyRestriction, WorkloadFamily, WorkloadKind, WorkloadRestriction};
use crate::envelope::CardKind;
use crate::ids::{CardName, CardUid};
fn family() -> WorkloadFamily {
WorkloadFamily {
kind: WorkloadKind::Service,
name: CardName::new("checkout").expect("static name"),
}
}
fn uid() -> CardUid {
CardUid::from_uuid(uuid::Uuid::now_v7()).expect("v7")
}
#[test]
fn restriction_and_access_use_closed_wire_unions() {
assert_eq!(
serde_json::to_value(WorkloadRestriction::Unrestricted).expect("serializes"),
json!({"mode": "unrestricted"})
);
assert_eq!(
serde_json::to_value(WorkloadRestriction::Family(family())).expect("serializes"),
json!({"mode": "family", "kind": "Service", "name": "checkout"})
);
assert_eq!(
serde_json::to_value(CardAccess::RootRequired {
restriction: FamilyRestriction::Family(family()),
})
.expect("serializes"),
json!({"mode": "root_required", "restriction": {"mode": "family", "kind": "Service", "name": "checkout"}})
);
let card = uid();
assert_eq!(
serde_json::to_value(CardAccess::Scoped {
cards: vec![card.clone()]
})
.expect("serializes"),
json!({"mode": "scoped", "cards": [card.as_str()]})
);
for refused in [
json!({"mode": "root_required", "restriction": {"mode": "unrestricted"}}),
json!({"mode": "scoped"}),
json!({"mode": "unrestricted"}),
] {
assert!(
serde_json::from_value::<CardAccess>(refused.clone()).is_err(),
"{refused} must not parse as Card access"
);
}
for kind in ["Model", "Agent"] {
assert!(
serde_json::from_value::<WorkloadRestriction>(
json!({"mode": "family", "kind": kind, "name": "m"})
)
.is_err(),
"{kind} is not a workload family kind"
);
}
assert_eq!(WorkloadKind::from_card_kind(&CardKind::Agent), None);
}
#[test]
fn intersection_is_exact_and_refuses_unresolved_roots() {
let (a, b, c) = (uid(), uid(), uid());
let left = CardAccess::Scoped {
cards: vec![a.clone(), b.clone()],
};
let right = CardAccess::Scoped {
cards: vec![b.clone(), c],
};
assert_eq!(
left.intersection(&right),
Some(CardAccess::Scoped { cards: vec![b] })
);
assert_eq!(
CardAccess::AnyRegistered.intersection(&left),
Some(left.clone())
);
assert_eq!(
left.intersection(&CardAccess::Scoped { cards: vec![] }),
Some(CardAccess::Scoped { cards: vec![] })
);
let unresolved = CardAccess::RootRequired {
restriction: FamilyRestriction::Family(family()),
};
assert_eq!(unresolved.intersection(&left), None);
assert_eq!(left.intersection(&unresolved), None);
assert!(WorkloadRestriction::Family(family()).allows(
&CardKind::Service,
&CardName::new("checkout").expect("name")
));
assert!(
!WorkloadRestriction::Family(family())
.allows(&CardKind::Agent, &CardName::new("checkout").expect("name"))
);
assert!(
WorkloadRestriction::Unrestricted
.allows(&CardKind::Agent, &CardName::new("anything").expect("name"))
);
}
#[test]
fn workload_uid_selector_accepts_one_card_uid() {
let uid: &[u8] = b"01890a5d-ac96-774b-bcce-b302099a8057";
assert_eq!(super::workload_uid_selector([]).expect("absent"), None);
assert_eq!(
super::workload_uid_selector([uid])
.expect("one uid")
.map(|uid| uid.to_string()),
Some("01890a5d-ac96-774b-bcce-b302099a8057".to_owned())
);
for values in [
vec![uid, uid],
vec![b"not-a-uid".as_slice()],
vec![b"\xff".as_slice()],
] {
assert!(matches!(
super::workload_uid_selector(values),
Err(crate::error::WyrdError::WorkloadUidRefused { .. })
));
}
}
#[test]
fn deployment_environment_selector_accepts_one_value() {
assert_eq!(
super::deployment_environment_selector([]).expect("absent"),
None
);
assert_eq!(
super::deployment_environment_selector([b"prod".as_slice()]).expect("one value"),
Some("prod".to_owned())
);
for values in [
vec![b"prod".as_slice(), b"prod".as_slice()],
vec![b"".as_slice()],
vec![b"\xff".as_slice()],
] {
assert!(matches!(
super::deployment_environment_selector(values),
Err(crate::error::WyrdError::Validation { .. })
));
}
}
}