use monetize_embed::fleet::{kind, GrowGoAhead, SignedOrder};
use monetize_embed::signing::Snapshot;
use monetize_embed::VerifyingKey;
use monetize_estate_side::pump::Checked;
use monetize_product::pull::{order as pkind, AddPrincipal, GrowRoom, PurgeTenant};
use monetize_product::EntitlementFact;
use serde::Deserialize;
pub use monetize_estate_side::pump::Addressee;
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
#[serde(default)]
pub struct Recharge {
pub main: String,
pub twin: String,
pub accept_lost: bool,
pub epoch: u64,
pub fence: Option<String>,
pub fenced: Option<String>,
pub case: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Order {
Entitlement(EntitlementFact),
RestateEntitlements(Snapshot),
PurgeTenant(PurgeTenant),
GrowRoom(GrowRoom),
GrowGoAhead(GrowGoAhead),
RechargeFromTwin(Recharge),
FencePrimary {
primary: String,
epoch: u64,
},
AdoptTwin {
twin: String,
principal: String,
},
BecomeTwin {
primary: String,
},
ReviewTwin,
AddPrincipal(AddPrincipal),
SignInAgain(SignInAgain),
Refeed(Refeed),
}
pub mod refeed_key {
pub const SMTP_URL: &str = "secret.smtp_url";
pub const SMTP_FROM: &str = "secret.smtp_from";
pub const SMTP_CA: &str = "secret.smtp_ca";
pub const PUBLIC_REPOSITORIES: &str = "public_repositories";
pub const PUBLIC_SITE: &str = "console_public_site";
pub const KNOWN: [&str; 5] = [
SMTP_URL,
SMTP_FROM,
SMTP_CA,
PUBLIC_REPOSITORIES,
PUBLIC_SITE,
];
pub const AS_SETTINGS: [&str; 1] = [PUBLIC_SITE];
}
pub const PUBLIC_SITE_SETTING: &str = "console.public-site";
#[derive(Clone, PartialEq, Eq, Default, Deserialize, serde::Serialize)]
#[serde(default)]
pub struct Refeed {
pub keys: std::collections::BTreeMap<String, String>,
}
impl std::fmt::Debug for Refeed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Refeed")
.field("keys", &self.keys.keys().collect::<Vec<_>>())
.finish()
}
}
impl Refeed {
pub fn get(&self, key: &str) -> Option<&str> {
self.keys
.get(key)
.map(|v| v.trim())
.filter(|v| !v.is_empty())
}
pub fn public_site(&self) -> Option<bool> {
match self
.keys
.get(refeed_key::PUBLIC_SITE)
.map(|v| v.trim().to_ascii_lowercase())
.as_deref()
{
Some("on") => Some(true),
Some("off") => Some(false),
_ => None,
}
}
pub fn settings(&self) -> Vec<(&'static str, Vec<u8>)> {
self.public_site()
.map(|on| {
vec![(
PUBLIC_SITE_SETTING,
if on { b"on".to_vec() } else { b"off".to_vec() },
)]
})
.into_iter()
.flatten()
.collect()
}
pub fn kept_keys(&self) -> Refeed {
Refeed {
keys: self
.keys
.iter()
.filter(|(k, _)| !refeed_key::AS_SETTINGS.contains(&k.as_str()))
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
#[serde(default)]
pub struct SignInAgain {
pub account: String,
pub to: String,
pub host: String,
pub ttl_secs: u64,
}
pub mod holger_kind {
pub const BECOME_TWIN: &str = "become_twin";
pub const REFEED: &str = "refeed";
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct PrimaryBody {
primary: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Verdict {
Forged(String),
Refused(String),
Run(Order),
}
#[derive(Deserialize)]
struct RestateBody {
snapshot: Snapshot,
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct FenceBody {
primary: String,
epoch: u64,
}
#[derive(Deserialize, Default)]
#[serde(default)]
struct AdoptBody {
twin: String,
principal: String,
}
pub fn accept(
o: &SignedOrder,
me: &Addressee,
key: &VerifyingKey,
last_seq: u64,
now_unix_ms: u64,
) -> Verdict {
match monetize_estate_side::pump::check(o, me, key, last_seq, now_unix_ms) {
Checked::Forged(why) => Verdict::Forged(why),
Checked::Refused(why) => Verdict::Refused(why),
Checked::Fresh => match decode(o.seq, &o.kind, &o.body) {
Ok(order) => Verdict::Run(order),
Err(why) => Verdict::Refused(why),
},
}
}
pub fn decode(seq: u64, kind: &str, body: &str) -> Result<Order, String> {
let body = if body.trim().is_empty() { "{}" } else { body };
let bad =
|e: serde_json::Error| format!("order {seq} ({kind}) has a body that does not decode: {e}");
Ok(match kind {
pkind::ENTITLEMENT => Order::Entitlement(serde_json::from_str(body).map_err(bad)?),
kind::RESTATE_ENTITLEMENTS => Order::RestateEntitlements(serde_json::from_str::<RestateBody>(body).map_err(bad)?.snapshot),
pkind::PURGE_TENANT => {
let p: PurgeTenant = serde_json::from_str(body).map_err(bad)?;
if p.tenant.trim().is_empty() {
return Err(format!("order {seq} is a purge_tenant that names no repository"));
}
Order::PurgeTenant(p)
}
pkind::GROW_ROOM => {
let g: GrowRoom = serde_json::from_str(body).map_err(bad)?;
if g.need_bytes == 0 {
return Err(format!("order {seq} is a grow_room that wants nothing"));
}
Order::GrowRoom(g)
}
kind::GROW_GO_AHEAD => {
let g: GrowGoAhead = serde_json::from_str(body).map_err(bad)?;
if g.go_ahead.trim().is_empty() || g.data_gib == 0 {
return Err(format!("order {seq} is a grow_go_ahead without monetize's signed step or a size"));
}
Order::GrowGoAhead(g)
}
kind::RECHARGE_FROM_TWIN => {
let mut r: Recharge = serde_json::from_str(body).map_err(bad)?;
r.fence = r.fence.filter(|f| !f.trim().is_empty());
r.fenced = r.fenced.filter(|f| !f.trim().is_empty());
if r.main.trim().is_empty() || r.twin.trim().is_empty() {
return Err(format!("order {seq} is a recharge_from_twin that names no main or no twin"));
}
Order::RechargeFromTwin(r)
}
kind::FENCE_PRIMARY => {
let b: FenceBody = serde_json::from_str(body).map_err(bad)?;
Order::FencePrimary { primary: b.primary, epoch: b.epoch }
}
kind::ADOPT_TWIN => {
let b: AdoptBody = serde_json::from_str(body).map_err(bad)?;
if b.twin.trim().is_empty() {
return Err(format!("order {seq} is an adopt_twin that names no twin"));
}
Order::AdoptTwin { twin: b.twin, principal: b.principal }
}
holger_kind::BECOME_TWIN => {
let b: PrimaryBody = serde_json::from_str(body).map_err(bad)?;
if b.primary.trim().is_empty() {
return Err(format!("order {seq} is a {kind} that names no primary"));
}
Order::BecomeTwin { primary: b.primary }
}
kind::REVIEW_TWIN => Order::ReviewTwin,
pkind::ADD_PRINCIPAL => {
let a: AddPrincipal = serde_json::from_str(body).map_err(bad)?;
if a.username.trim().is_empty() || a.signup.trim().is_empty() {
return Err(format!("order {seq} is an add_principal that names no repository or no signup"));
}
Order::AddPrincipal(a)
}
pkind::ADD_USERS => return Err(format!("order {seq}: Holger has no seats to grant on an account (a Holger tenant is a repository), so add_users is refused")),
kind::SIGN_IN_AGAIN => {
let b: SignInAgain = serde_json::from_str(body).map_err(bad)?;
if let Err(why) = crate::sign_in::check(&b.account, &b.to, &b.host) {
return Err(format!("order {seq} is a sign_in_again this box cannot mail: {why}"));
}
Order::SignInAgain(b)
}
holger_kind::REFEED => {
let r: Refeed = serde_json::from_str(body).map_err(|_| format!("order {seq} (refeed) has a body that does not decode as estate keys"))?;
if r.keys.is_empty() {
return Err(format!("order {seq} is a refeed that names no estate key"));
}
if let Some(k) = r.keys.keys().find(|k| !refeed_key::KNOWN.contains(&k.as_str())) {
return Err(format!("order {seq} is a refeed of the estate key {k:?}, which Holger does not take again (it takes {}): nothing was kept", refeed_key::KNOWN.join(", ")));
}
match (r.get(refeed_key::SMTP_URL), r.get(refeed_key::SMTP_FROM)) {
(Some(url), Some(_)) if !(url.starts_with("smtp://") || url.starts_with("smtps://")) => return Err(format!("order {seq} is a refeed whose {} is not smtp:// or smtps://", refeed_key::SMTP_URL)),
(Some(_), Some(_)) => {}
(None, None) if r.get(refeed_key::SMTP_CA).is_none() => {}
_ => return Err(format!("order {seq} is a refeed with half a mail relay ({} and {} go together)", refeed_key::SMTP_URL, refeed_key::SMTP_FROM)),
}
if let Some(v) = r.keys.get(refeed_key::PUBLIC_SITE) {
if r.public_site().is_none() {
return Err(format!("order {seq} is a refeed of {} = {:?}, which is neither `on` nor `off`: nothing was kept", refeed_key::PUBLIC_SITE, v.trim()));
}
}
if let Some(list) = r.keys.get(refeed_key::PUBLIC_REPOSITORIES).filter(|l| !l.trim().is_empty()) {
if let Some(bad) = list.split(',').map(str::trim).find(|n| n.is_empty() || !n.bytes().all(|c| c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'.'))) {
return Err(format!("order {seq} is a refeed whose {} names {bad:?}: a repository is one plain word", refeed_key::PUBLIC_REPOSITORIES));
}
}
Order::Refeed(r)
}
other => return Err(format!("order {seq}: Holger does not know the order kind {other:?}")),
})
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use ed25519_dalek::Signer as _;
use monetize_embed::fleet::order_message;
pub const MONETIZE_SEED: [u8; 32] = [7u8; 32];
pub fn me(role: &str) -> Addressee {
Addressee {
product: "holger".into(),
instance: "acme".into(),
role: role.into(),
}
}
pub fn monetize_key() -> VerifyingKey {
ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED).verifying_key()
}
pub fn signed(seq: u64, role: &str, kind: &str, body: &str, signer: [u8; 32]) -> SignedOrder {
let mut o = SignedOrder {
seq,
product: "holger".into(),
instance: "acme".into(),
role: role.into(),
kind: kind.into(),
body: body.into(),
issued_unix_ms: 9_000,
not_after_unix_ms: 0,
signature: Vec::new(),
};
o.signature = ed25519_dalek::SigningKey::from_bytes(&signer)
.sign(&order_message(&o))
.to_bytes()
.to_vec();
o
}
#[test]
fn a_forged_order_is_forged_whatever_it_claims() {
let o = signed(9, "main", "review_twin", "{}", [9u8; 32]);
assert!(matches!(
accept(&o, &me("main"), &monetize_key(), 0, 10_000),
Verdict::Forged(_)
));
}
#[test]
fn an_order_for_the_twin_a_replay_and_an_expired_one_are_refused_by_name() {
let k = monetize_key();
let twin = signed(2, "twin", "review_twin", "{}", MONETIZE_SEED);
assert!(
matches!(accept(&twin, &me("main"), &k, 0, 10_000), Verdict::Refused(w) if w.contains("twin"))
);
let old = signed(2, "main", "review_twin", "{}", MONETIZE_SEED);
assert!(
matches!(accept(&old, &me("main"), &k, 2, 10_000), Verdict::Refused(w) if w.contains("replay"))
);
let mut late = signed(3, "main", "review_twin", "{}", MONETIZE_SEED);
late.not_after_unix_ms = 5_000;
late.signature = ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED)
.sign(&order_message(&late))
.to_bytes()
.to_vec();
assert!(
matches!(accept(&late, &me("main"), &k, 0, 10_000), Verdict::Refused(w) if w.contains("expired"))
);
}
#[test]
fn a_refeed_carries_the_public_site_switch_and_a_switch_that_is_neither_on_nor_off_is_refused()
{
for (body, want) in [
(r#"{"keys":{"console_public_site":"on"}}"#, true),
(r#"{"keys":{"console_public_site":" OFF "}}"#, false),
(
r#"{"keys":{"console_public_site":"on","public_repositories":"crates"}}"#,
true,
),
] {
let o = signed(7, "main", "refeed", body, MONETIZE_SEED);
match accept(&o, &me("main"), &monetize_key(), 0, 10_000) {
Verdict::Run(Order::Refeed(r)) => {
assert_eq!(r.public_site(), Some(want), "{body}");
assert_eq!(
r.settings(),
vec![(
PUBLIC_SITE_SETTING,
if want {
b"on".to_vec()
} else {
b"off".to_vec()
}
)],
"{body}: the switch is the console's generic setting"
);
assert!(
!r.kept_keys().keys.contains_key(refeed_key::PUBLIC_SITE),
"{body}: the switch is no key kept beside the feed"
);
}
other => panic!("{body}: {other:?}"),
}
}
let o = signed(
8,
"main",
"refeed",
r#"{"keys":{"console_public_site":"maybe"}}"#,
MONETIZE_SEED,
);
match accept(&o, &me("main"), &monetize_key(), 0, 10_000) {
Verdict::Refused(w) => assert!(
w.contains("refeed") && w.contains("console_public_site") && w.contains("maybe"),
"{w}"
),
other => panic!("{other:?}"),
}
}
#[test]
fn a_refeed_is_run_with_the_keys_it_carries_and_a_key_holger_does_not_take_is_refused_by_name()
{
let body = r#"{"keys":{"secret.smtp_url":"smtps://holger%40holger.rs:t0kenVALUE@relay.mock.invalid:465","secret.smtp_from":"Holger <holger@mock.invalid>","public_repositories":"crates,npm"}}"#;
let o = signed(4, "main", "refeed", body, MONETIZE_SEED);
match accept(&o, &me("main"), &monetize_key(), 0, 10_000) {
Verdict::Run(Order::Refeed(r)) => {
assert_eq!(
(
r.get(refeed_key::SMTP_FROM),
r.get(refeed_key::PUBLIC_REPOSITORIES)
),
(Some("Holger <holger@mock.invalid>"), Some("crates,npm"))
);
assert!(!format!("{r:?}").contains("t0kenVALUE"), "{r:?}");
}
other => panic!("{other:?}"),
}
let o = signed(
5,
"main",
"refeed",
r#"{"keys":{"public_repositories":"crates"}}"#,
MONETIZE_SEED,
);
assert!(
matches!(
accept(&o, &me("main"), &monetize_key(), 0, 10_000),
Verdict::Run(Order::Refeed(_))
),
"the public list alone"
);
let o = signed(
5,
"main",
"refeed",
r#"{"keys":{"public_repositories":""}}"#,
MONETIZE_SEED,
);
match accept(&o, &me("main"), &monetize_key(), 0, 10_000) {
Verdict::Run(Order::Refeed(r)) => assert_eq!(
r.keys
.get(refeed_key::PUBLIC_REPOSITORIES)
.map(String::as_str),
Some(""),
"none public"
),
other => panic!("a cleared public list is refused: {other:?}"),
}
for (bad, word) in [
(r#"{"keys":{}}"#, "names no estate key"),
(
r#"{"keys":{"console_admin_token":"t0kenVALUE"}}"#,
"console_admin_token",
),
(
r#"{"keys":{"secret.smtp_url":"smtps://u:t0kenVALUE@r.mock.invalid"}}"#,
"half a mail relay",
),
(
r#"{"keys":{"secret.smtp_url":"http://u:t0kenVALUE@r.mock.invalid","secret.smtp_from":"h@mock.invalid"}}"#,
"not smtp://",
),
(
r#"{"keys":{"public_repositories":"crates,../x"}}"#,
"one plain word",
),
(r#"{"keys":7}"#, "does not decode"),
] {
let o = signed(6, "main", "refeed", bad, MONETIZE_SEED);
match accept(&o, &me("main"), &monetize_key(), 0, 10_000) {
Verdict::Refused(w) => assert!(
w.contains("refeed") && w.contains(word) && !w.contains("t0kenVALUE"),
"{bad}: {w}"
),
other => panic!("{bad}: {other:?}"),
}
}
}
#[test]
fn holgers_missing_concepts_are_refused_by_name() {
let o = signed(
1,
"main",
"add_principal",
r#"{"username":"crates","kind":"org","signup":"s-1"}"#,
MONETIZE_SEED,
);
assert!(
matches!(accept(&o, &me("main"), &monetize_key(), 0, 10_000), Verdict::Run(Order::AddPrincipal(a)) if a.username == "crates" && a.signup == "s-1"),
"a signup is a repository on a plan"
);
let o = signed(
2,
"main",
"add_principal",
r#"{"username":"","signup":"s-1"}"#,
MONETIZE_SEED,
);
assert!(
matches!(accept(&o, &me("main"), &monetize_key(), 0, 10_000), Verdict::Refused(w) if w.contains("names no repository"))
);
for (kind, word) in [("add_users", "add_users"), ("make_coffee", "make_coffee")] {
let o = signed(
1,
"main",
kind,
r#"{"username":"a","signup":"s"}"#,
MONETIZE_SEED,
);
match accept(&o, &me("main"), &monetize_key(), 0, 10_000) {
Verdict::Refused(w) => assert!(w.contains(word), "{w}"),
other => panic!("{kind}: {other:?}"),
}
}
}
#[test]
fn a_sign_in_again_is_run_and_a_body_a_link_cannot_carry_is_refused() {
let body = r#"{"account":"acme","to":"buyer@mock.invalid","host":"acme.holger.mock.invalid","ttl_secs":86400}"#;
let o = signed(5, "main", "sign_in_again", body, MONETIZE_SEED);
match accept(&o, &me("main"), &monetize_key(), 0, 10_000) {
Verdict::Run(Order::SignInAgain(s)) => assert_eq!(
(
s.account.as_str(),
s.to.as_str(),
s.host.as_str(),
s.ttl_secs
),
(
"acme",
"buyer@mock.invalid",
"acme.holger.mock.invalid",
86_400
)
),
other => panic!("{other:?}"),
}
for bad in [
r#"{"account":"a&user=x","to":"b@mock.invalid","host":"h.mock.invalid"}"#,
r#"{"account":"a","to":"b@mock.invalid\r\nBcc: x@mock.invalid","host":"h.mock.invalid"}"#,
r#"{"account":"a","to":"b@mock.invalid","host":""}"#,
] {
let o = signed(6, "main", "sign_in_again", bad, MONETIZE_SEED);
assert!(
matches!(accept(&o, &me("main"), &monetize_key(), 0, 10_000), Verdict::Refused(w) if w.contains("sign_in_again")),
"{bad}"
);
}
}
}