Skip to main content

mail4agent_server/
policy.rs

1//! Neutral policy hook. The server asks the hook before an action; the
2//! default allows everything. The server defines no tariff or tier: the
3//! product's assertion carries opaque claims (see [`claims_from`]) and the
4//! hook decides what they mean.
5
6use std::collections::BTreeMap;
7
8use crate::store::RoomKind;
9
10/// Opaque product claims for one caller.
11pub type Claims = BTreeMap<String, String>;
12
13/// Claims for a caller admitted by a signed assertion: `flag` ("0"/"1", opaque).
14pub fn claims_from(paid_flag: u8) -> Claims {
15    let mut c = Claims::new();
16    c.insert("flag".into(), paid_flag.to_string());
17    c
18}
19
20/// Things the server asks about.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum Action {
23    CreateRoom,
24    Invite,
25    JoinRoom,
26    SendEvent,
27    UploadMedia,
28}
29
30#[derive(Debug, Clone)]
31pub struct PolicyContext<'a> {
32    pub claims: &'a Claims,
33    pub action: Action,
34    /// Known when the action concerns a room kind.
35    pub room_kind: Option<RoomKind>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum Decision {
40    Allow,
41    /// Refuse with a human-readable reason (shown in `M4A_POLICY_DENIED`).
42    Deny(String),
43}
44
45pub trait PolicyHook: Send + Sync {
46    fn decide(&self, ctx: &PolicyContext<'_>) -> Decision;
47}
48
49/// Allows everything.
50pub struct AllowAll;
51
52impl PolicyHook for AllowAll {
53    fn decide(&self, _ctx: &PolicyContext<'_>) -> Decision {
54        Decision::Allow
55    }
56}
57
58/// Run the hook and turn a denial into the API error.
59pub fn enforce(hook: &dyn PolicyHook, claims: &Claims, action: Action, room_kind: Option<RoomKind>) -> Result<(), crate::error::MatrixError> {
60    match hook.decide(&PolicyContext { claims, action, room_kind }) {
61        Decision::Allow => Ok(()),
62        Decision::Deny(why) => Err(crate::error::MatrixError::policy_denied(why)),
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    struct DenyFlaglessChannels;
71    impl PolicyHook for DenyFlaglessChannels {
72        fn decide(&self, c: &PolicyContext<'_>) -> Decision {
73            if c.action == Action::CreateRoom && c.room_kind == Some(RoomKind::Channel) && c.claims.get("flag").map(String::as_str) != Some("1") {
74                Decision::Deny("not available".into())
75            } else {
76                Decision::Allow
77            }
78        }
79    }
80
81    #[test]
82    fn default_allows_and_custom_hook_denies_with_policy_error() {
83        let c0 = claims_from(0);
84        assert!(enforce(&AllowAll, &c0, Action::CreateRoom, Some(RoomKind::Channel)).is_ok());
85        let e = enforce(&DenyFlaglessChannels, &c0, Action::CreateRoom, Some(RoomKind::Channel)).unwrap_err();
86        assert_eq!((e.status, e.errcode), (403, "M4A_POLICY_DENIED"));
87        assert!(enforce(&DenyFlaglessChannels, &c0, Action::CreateRoom, Some(RoomKind::Dm)).is_ok());
88        assert!(enforce(&DenyFlaglessChannels, &claims_from(1), Action::CreateRoom, Some(RoomKind::Channel)).is_ok());
89    }
90}