Skip to main content

realtime/server/
policy.rs

1use super::{ChannelName, ConnectionMeta, RealtimeError};
2
3pub trait ChannelPolicy: Send + Sync {
4    fn can_join(&self, meta: &ConnectionMeta, channel: &ChannelName) -> Result<(), RealtimeError>;
5    fn can_publish(
6        &self,
7        meta: &ConnectionMeta,
8        channel: &ChannelName,
9        event: &str,
10    ) -> Result<(), RealtimeError>;
11}
12
13#[derive(Debug, Default)]
14pub struct DefaultChannelPolicy;
15
16impl ChannelPolicy for DefaultChannelPolicy {
17    fn can_join(&self, meta: &ConnectionMeta, channel: &ChannelName) -> Result<(), RealtimeError> {
18        let name = channel.as_str();
19        if let Some(user) = name.strip_prefix("user:") {
20            if user == meta.user_id || is_admin(meta) {
21                return Ok(());
22            }
23            return Err(RealtimeError::forbidden(
24                "Cannot join another user's private channel",
25            ));
26        }
27        if name.starts_with("admin:") && !is_admin(meta) {
28            return Err(RealtimeError::forbidden(
29                "Admin channel requires admin role",
30            ));
31        }
32        Ok(())
33    }
34
35    fn can_publish(
36        &self,
37        meta: &ConnectionMeta,
38        channel: &ChannelName,
39        event: &str,
40    ) -> Result<(), RealtimeError> {
41        if event.trim().is_empty() {
42            return Err(RealtimeError::bad_request("Event name is required"));
43        }
44        let name = channel.as_str();
45        if let Some(user) = name.strip_prefix("user:") {
46            if user == meta.user_id || is_admin(meta) {
47                return Ok(());
48            }
49            return Err(RealtimeError::forbidden(
50                "Cannot publish to another user's private channel",
51            ));
52        }
53        if name.starts_with("admin:") && !is_admin(meta) {
54            return Err(RealtimeError::forbidden(
55                "Admin channel requires admin role",
56            ));
57        }
58        Ok(())
59    }
60}
61
62fn is_admin(meta: &ConnectionMeta) -> bool {
63    meta.roles.iter().any(|role| role == "admin")
64}
65
66#[cfg(test)]
67mod tests {
68    use uuid::Uuid;
69
70    use super::*;
71    use crate::server::ConnectionId;
72
73    fn connection_meta(user_id: &str, roles: Vec<String>) -> ConnectionMeta {
74        ConnectionMeta {
75            id: ConnectionId(Uuid::new_v4()),
76            user_id: user_id.to_string(),
77            roles,
78            joined_at_unix: 0,
79        }
80    }
81
82    #[test]
83    fn user_cannot_join_another_private_channel() {
84        let policy = DefaultChannelPolicy;
85        let user_meta = connection_meta("u1", vec!["user".to_string()]);
86        let channel = ChannelName::parse("user:u2").expect("channel should parse");
87
88        let err = policy
89            .can_join(&user_meta, &channel)
90            .expect_err("join should be denied");
91        assert_eq!(err.message(), "Cannot join another user's private channel");
92    }
93
94    #[test]
95    fn admin_can_join_another_private_channel() {
96        let policy = DefaultChannelPolicy;
97        let admin_meta = connection_meta("admin", vec!["admin".to_string(), "user".to_string()]);
98        let channel = ChannelName::parse("user:u2").expect("channel should parse");
99
100        policy
101            .can_join(&admin_meta, &channel)
102            .expect("admin should be allowed");
103    }
104
105    #[test]
106    fn user_publish_to_admin_channel_is_denied() {
107        let policy = DefaultChannelPolicy;
108        let user_meta = connection_meta("u1", vec!["user".to_string()]);
109        let channel = ChannelName::parse("admin:ops").expect("channel should parse");
110
111        let err = policy
112            .can_publish(&user_meta, &channel, "status.updated")
113            .expect_err("publish should be denied");
114        assert_eq!(err.message(), "Admin channel requires admin role");
115    }
116}