Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-site-buy/src/site.rs

//! **The provider trait a product site implements** (the plug-in shape of monetize-cloud's
//! `DnsWriter`): who is signed in, the actor tickets the product vouches for its buyer with,
//! the caps the buyer holds, and whether a paid sign-up's account has its passkey yet.
//! Everything else in a buy — the calls, the URLs, the refusals, the words — is the relay's.

use std::collections::BTreeMap;
use std::future::Future;

use axum::http::{HeaderMap, StatusCode};

use crate::urls::Pages;

/// The ticket purposes a buy asks for (monetize-embed `ticket::PURPOSE_*`).
pub const PURPOSE_ESTATE: &str = "estate";
pub const PURPOSE_ORDER: &str = "order";
pub const PURPOSE_RENEW: &str = "renew";
pub const PURPOSE_READ: &str = "read";
/// A cancel of an order not yet paid: covers no amount.
pub const PURPOSE_CANCEL: &str = "cancel";

/// **A refusal of the site's own** (not the shop's): the HTTP status and the sentence the
/// buyer reads. Never an internal word: the site writes it for the buyer.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Refusal {
    pub status: StatusCode,
    pub words: String,
}

impl Refusal {
    pub fn new(status: StatusCode, words: impl Into<String>) -> Refusal {
        Refusal { status, words: words.into() }
    }
}

/// **A member of an account** (an employee, an admin-appointed member): buys nothing; the
/// account's admin buys for it.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Member {
    /// The account they are a member of.
    pub account: String,
    /// Its admin (empty: not known).
    pub admin: String,
    /// The site's sentence for this membership ("You are a member of acme.").
    pub line: String,
}

/// **The person signed in.**
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Buyer {
    /// The account a private instance is ordered by (`x14`).
    pub account: String,
    /// The tenant the account's room is metered as at monetize (Gunnar: its namespace; Holger:
    /// the account's repository). Empty: the account.
    pub tenant: String,
    /// Set when the person is a member of an account and not its buyer.
    pub member: Option<Member>,
    /// The payer an order names (Gunnar: the operator's receipts address when the operator
    /// buys; empty for a tenant, and monetize uses the address the account signed up with).
    pub payer: String,
}

impl Buyer {
    /// The tenant, else the account.
    pub fn tenant(&self) -> &str {
        if self.tenant.is_empty() {
            &self.account
        } else {
            &self.tenant
        }
    }
}

/// **Who asks.**
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Who {
    /// Nobody signed in.
    Visitor,
    /// A credential that is no person (a break-glass bearer, an operator token): buys nothing.
    Machine,
    /// A person signed in.
    Person(Buyer),
}

/// **An administrator placing an order on a tenant's behalf** (Gunnar's console's operator):
/// the operator door group's caller, as the site vouches for it.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Operator {
    /// Who, for the audit line.
    pub who: String,
    /// The payer the order names (Gunnar: the operator's receipts address; empty: monetize uses
    /// the address the tenant signed up with).
    pub payer: String,
}

/// What a site that names no operator answers on the operator's doors.
pub const NO_OPERATOR_HERE: &str = "an operator's orders are not taken here";

/// **Whether a paid sign-up's account may register its passkey now.**
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Account {
    /// It exists and holds no passkey: the invite link is handed out.
    NoPasskey,
    /// It holds one already: sign in with it.
    HasPasskey,
}

/// The code a refusal carries when the person must sign in again with a fresh touch of their
/// key first (Gunnar's console's `STEP_UP_CODE`): the page offers the login, not a retry.
pub const STEP_UP_CODE: &str = "stepup";

/// What a site that cannot prove a fresh login says (Gunnar's step-up sentence, its window).
pub const STEP_UP_WORDS: &str = "This needs a fresh touch of your key (a user-verified login from the last 10 minutes).";

/// **The product site.** Implemented once per product (Gunnar's console, Holger's front).
pub trait BuySite: Send + Sync + 'static {
    /// This site's origin, `https://holger.rs` (its passkeys' relying party): every way back
    /// from the vendor is on it.
    fn origin(&self) -> String;

    /// The product as the buyer reads it: "Gunnar", "Holger".
    fn product_word(&self) -> String;

    /// The site's sign-up choices as its page spells them.
    fn pages(&self) -> Pages {
        Pages::default()
    }

    /// The site's budget for `door` (`plans`, `name`, `start`, `status`, …) per client address
    /// `ip` and `key`; `false` answers 429 before the shop is dialled. Default: no budget.
    fn allowed(&self, door: &str, ip: &str, key: &str) -> bool {
        let _ = (door, ip, key);
        true
    }

    /// **The proxies whose `X-Forwarded-For` [`router`](crate::router) believes**, besides
    /// loopback, which is always believed (a reverse proxy on the same box: Gunnar's caddy).
    /// From any other peer the header is ignored and the socket's address is the client's
    /// (Gunnar's `GUNNAR_UI_TRUSTED_PROXIES` rule). Default: none, loopback only.
    fn trusted_proxies(&self) -> &[std::net::IpAddr] {
        &[]
    }

    /// **The fixed buy link** a site falls back to where the shop mints no checkout for a
    /// sign-up (`UNIMPLEMENTED`; Gunnar's `GUNNAR_UI_BUY_URL`). Default: none.
    fn fixed_buy_url(&self) -> Option<String> {
        None
    }

    /// **Whether the person signed in on this request touched their key recently** (a
    /// user-verified login inside the site's step-up window): what ending an instance needs.
    /// `Err(words)`: they sign in again first. Default: no site proves it, so it is refused.
    fn stepped_up(&self, headers: &HeaderMap) -> impl Future<Output = Result<(), String>> + Send {
        let _ = headers;
        async { Err(STEP_UP_WORDS.to_owned()) }
    }

    /// **Who may order for a tenant named in the path** (the operator door group, mounted only
    /// where a site names it: [`Doors::operator`](crate::Doors)): an administrator, as the site
    /// decides it from the request's session. The order then carries no actor ticket: nobody
    /// on these doors claims to hold the tenant. Default: nobody (403).
    fn operator(&self, headers: &HeaderMap) -> impl Future<Output = Result<Operator, Refusal>> + Send {
        let _ = headers;
        async { Err(Refusal::new(StatusCode::FORBIDDEN, NO_OPERATOR_HERE)) }
    }

    /// **Who asks**, from the request's own session (cookie, bearer).
    fn who(&self, headers: &HeaderMap) -> impl Future<Output = Result<Who, Refusal>> + Send;

    /// **An actor ticket for the buyer** (purpose [`PURPOSE_ESTATE`], [`PURPOSE_ORDER`] with the
    /// exact target `caps`, [`PURPOSE_RENEW`], [`PURPOSE_READ`], [`PURPOSE_CANCEL`]): the product vouches for the
    /// person over its own key. `headers` is the request (the person's own session).
    fn ticket(&self, headers: &HeaderMap, buyer: &Buyer, purpose: &str, caps: &BTreeMap<String, u64>) -> impl Future<Output = Result<Vec<u8>, Refusal>> + Send;

    /// **The caps the buyer's tenant holds now** (an `order` ticket names the target).
    fn held_caps(&self, headers: &HeaderMap, buyer: &Buyer) -> impl Future<Output = Result<BTreeMap<String, u64>, Refusal>> + Send;

    /// **The paid sign-up's account `user`, as the product holds it**: its passkey, or a
    /// refusal (no such person here yet: a 409 the welcome page keeps polling through).
    fn signup_account(&self, user: &str) -> impl Future<Output = Result<Account, Refusal>> + Send;
}