Skip to main content

platform/realm/
acl.rs

1//! Actor 访问控制列表
2//!
3//! 定义了 Actor 的权限控制数据结构
4use anyhow::Result;
5
6use serde::{Deserialize, Serialize};
7
8use super::super::RealmError;
9use crate::storage::db::get_database;
10
11const ANONYMOUS_ACTOR_TYPE: &str = "ANONCLNT";
12const VOICE_ACTOR_TYPE: &str = "VOICE";
13const CHAT_ACTOR_TYPE: &str = "CHAT";
14
15/// Actor 访问控制列表
16///
17/// 管理不同类型 Actor 之间的访问权限
18#[derive(Debug, Serialize, Deserialize, Default)]
19pub struct ActorAcl {
20    pub rowid: Option<i64>,
21    /// 目标(被访问方)所在 realm
22    pub realm_id: u32,
23    /// 来源(访问方)所在 realm;None 表示未设置来源范围
24    pub source_realm_id: Option<u32>,
25    pub from_type: String,
26    pub to_type: String,
27    pub access: bool,
28}
29
30impl<'r> sqlx::FromRow<'r, sqlx::sqlite::SqliteRow> for ActorAcl {
31    fn from_row(row: &'r sqlx::sqlite::SqliteRow) -> Result<Self, sqlx::Error> {
32        use sqlx::Row;
33        let source_realm_id = row
34            .try_get::<Option<i64>, _>("source_realm_id")?
35            .and_then(|v| u32::try_from(v).ok());
36        Ok(Self {
37            rowid: row.try_get("rowid")?,
38            realm_id: row.try_get::<i64, _>("realm_id")?.try_into().unwrap(),
39            source_realm_id,
40            from_type: row.try_get("from_type")?,
41            to_type: row.try_get("to_type")?,
42            access: row.try_get::<i64, _>("access")? != 0,
43        })
44    }
45}
46
47impl ActorAcl {
48    /// 创建新的访问控制规则
49    pub fn new(realm_id: u32, from_type: String, to_type: String, access: bool) -> Self {
50        // 兼容历史语义:不显式指定来源 realm 时,默认仅同 realm 生效
51        Self::new_with_source_realm(realm_id, Some(realm_id), from_type, to_type, access)
52    }
53
54    /// 创建带来源 realm 约束的访问控制规则
55    pub fn new_with_source_realm(
56        realm_id: u32,
57        source_realm_id: Option<u32>,
58        from_type: String,
59        to_type: String,
60        access: bool,
61    ) -> Self {
62        Self {
63            rowid: None,
64            realm_id,
65            source_realm_id,
66            from_type: from_type.to_string(),
67            to_type: to_type.to_string(),
68            access,
69        }
70    }
71
72    /// Save ACL rule to database
73    ///
74    /// Inserts a new rule or updates existing one based on rowid.
75    ///
76    /// # Returns
77    ///
78    /// Returns the rowid of the saved ACL rule
79    pub async fn save(&mut self) -> Result<i64, RealmError> {
80        let db = get_database();
81        let pool = db.get_pool();
82
83        if let Some(rowid) = self.rowid {
84            // 更新现有记录
85            sqlx::query(
86                "UPDATE actoracl
87                 SET realm_id = ?, source_realm_id = ?, from_type = ?, to_type = ?, access = ?
88                 WHERE rowid = ?",
89            )
90            .bind(self.realm_id)
91            .bind(self.source_realm_id.map(i64::from))
92            .bind(&self.from_type)
93            .bind(&self.to_type)
94            .bind(if self.access { 1 } else { 0 })
95            .bind(rowid)
96            .execute(pool)
97            .await?;
98
99            Ok(rowid)
100        } else {
101            // 插入新记录
102            let result = sqlx::query(
103                "INSERT INTO actoracl (realm_id, source_realm_id, from_type, to_type, access)
104                 VALUES (?, ?, ?, ?, ?)",
105            )
106            .bind(self.realm_id)
107            .bind(self.source_realm_id.map(i64::from))
108            .bind(&self.from_type)
109            .bind(&self.to_type)
110            .bind(if self.access { 1 } else { 0 })
111            .execute(pool)
112            .await?;
113
114            let new_rowid = result.last_insert_rowid();
115            self.rowid = Some(new_rowid);
116            Ok(new_rowid)
117        }
118    }
119
120    /// Delete ACL rule by ID
121    ///
122    /// # Arguments
123    ///
124    /// - `id`: ACL rule rowid
125    ///
126    /// # Returns
127    ///
128    /// Returns number of deleted rows (0 or 1)
129    pub async fn delete_by_id(id: i64) -> Result<u64, RealmError> {
130        let db = get_database();
131        let pool = db.get_pool();
132
133        let result = sqlx::query("DELETE FROM actoracl WHERE rowid = ?")
134            .bind(id)
135            .execute(pool)
136            .await?;
137
138        let changes = result.rows_affected();
139        if changes > 0 {
140            Ok(changes)
141        } else {
142            Err(RealmError::NotFound)
143        }
144    }
145
146    /// Get ACL rule by ID
147    ///
148    /// # Arguments
149    ///
150    /// - `id`: ACL rule rowid
151    ///
152    /// # Returns
153    ///
154    /// Returns the ACL rule if found, None otherwise
155    pub async fn get(id: i64) -> Result<Option<Self>, RealmError> {
156        let db = get_database();
157        let pool = db.get_pool();
158
159        let result = sqlx::query_as::<_, ActorAcl>(
160            "SELECT rowid, realm_id, source_realm_id, from_type, to_type, access
161             FROM actoracl
162             WHERE rowid = ?",
163        )
164        .bind(id)
165        .fetch_optional(pool)
166        .await?;
167
168        Ok(result)
169    }
170
171    /// 获取指定 Realm 的所有访问控制规则
172    pub async fn get_by_realm(realm_id: u32) -> Result<Vec<Self>, RealmError> {
173        let db = get_database();
174        let pool = db.get_pool();
175
176        let acls = sqlx::query_as::<_, ActorAcl>(
177            "SELECT rowid, realm_id, source_realm_id, from_type, to_type, access
178             FROM actoracl
179             WHERE realm_id = ?",
180        )
181        .bind(realm_id)
182        .fetch_all(pool)
183        .await?;
184
185        Ok(acls)
186    }
187
188    /// 根据类型获取访问控制规则
189    pub async fn get_by_types(
190        target_realm_id: u32,
191        source_realm_id: u32,
192        from_type: &str,
193        to_type: &str,
194    ) -> Result<Option<Self>, RealmError> {
195        let db = get_database();
196        let pool = db.get_pool();
197
198        let result = sqlx::query_as::<_, ActorAcl>(
199            "SELECT rowid, realm_id, source_realm_id, from_type, to_type, access
200             FROM actoracl
201             WHERE realm_id = ? AND source_realm_id = ? AND from_type = ? AND to_type = ?
202             ORDER BY rowid DESC
203             LIMIT 1",
204        )
205        .bind(target_realm_id)
206        .bind(source_realm_id)
207        .bind(from_type)
208        .bind(to_type)
209        .fetch_optional(pool)
210        .await?;
211
212        Ok(result)
213    }
214
215    /// 删除指定目标类型的全部 ACL(用于服务重注册时替换旧 ACL)
216    pub async fn delete_by_target(realm_id: u32, to_type: &str) -> Result<u64, RealmError> {
217        let db = get_database();
218        let pool = db.get_pool();
219
220        let result = sqlx::query("DELETE FROM actoracl WHERE realm_id = ? AND to_type = ?")
221            .bind(realm_id)
222            .bind(to_type)
223            .execute(pool)
224            .await?;
225
226        Ok(result.rows_affected())
227    }
228
229    pub fn access(&self) -> bool {
230        self.access
231    }
232
233    /// Check if discovery is allowed between two actor types
234    ///
235    /// Used for Presence notification filtering and service discovery
236    ///
237    /// # Arguments
238    ///
239    /// - `source_realm_id`: 来源 Actor 的 realm
240    /// - `target_realm_id`: 目标 Actor 的 realm
241    /// - `from_type`: Source actor type
242    /// - `to_type`: Target actor type
243    ///
244    /// # Returns
245    ///
246    /// Returns true if discovery is allowed, false otherwise.
247    /// Default policy: deny if no rule exists (secure by default)
248    pub async fn can_discover(
249        source_realm_id: u32,
250        target_realm_id: u32,
251        from_type: &str,
252        to_type: &str,
253    ) -> Result<bool, RealmError> {
254        match Self::get_by_types(target_realm_id, source_realm_id, from_type, to_type).await? {
255            Some(acl) => {
256                crate::recording::debug!(
257                    "ACL rule found: source_realm_id={}, target_realm_id={}, from_type={}, to_type={}, access={}",
258                    source_realm_id,
259                    target_realm_id,
260                    from_type,
261                    to_type,
262                    acl.access()
263                );
264                Ok(acl.access())
265            }
266            None => {
267                // Default policy: deny if no rule exists
268                crate::recording::debug!(
269                    "No ACL rule found, denying discovery (default policy): source_realm_id={}, target_realm_id={}, from_type={}, to_type={}",
270                    source_realm_id,
271                    target_realm_id,
272                    from_type,
273                    to_type
274                );
275                Ok(false)
276            }
277        }
278    }
279}
280
281pub fn mock_actor_acl() -> Vec<ActorAcl> {
282    vec![
283        ActorAcl {
284            rowid: None,
285            realm_id: 2,
286            source_realm_id: Some(2),
287            from_type: ANONYMOUS_ACTOR_TYPE.to_string(),
288            to_type: VOICE_ACTOR_TYPE.to_string(),
289            access: true,
290        },
291        ActorAcl {
292            rowid: None,
293            realm_id: 2,
294            source_realm_id: Some(2),
295            from_type: ANONYMOUS_ACTOR_TYPE.to_string(),
296            to_type: CHAT_ACTOR_TYPE.to_string(),
297            access: true,
298        },
299        ActorAcl {
300            rowid: None,
301            realm_id: 2,
302            source_realm_id: Some(2),
303            from_type: ANONYMOUS_ACTOR_TYPE.to_string(),
304            to_type: ANONYMOUS_ACTOR_TYPE.to_string(),
305            access: false,
306        },
307        ActorAcl {
308            rowid: None,
309            realm_id: 2,
310            source_realm_id: Some(2),
311            from_type: CHAT_ACTOR_TYPE.to_string(),
312            to_type: ANONYMOUS_ACTOR_TYPE.to_string(),
313            access: true,
314        },
315        ActorAcl {
316            rowid: None,
317            realm_id: 2,
318            source_realm_id: Some(2),
319            from_type: CHAT_ACTOR_TYPE.to_string(),
320            to_type: VOICE_ACTOR_TYPE.to_string(),
321            access: true,
322        },
323        ActorAcl {
324            rowid: None,
325            realm_id: 2,
326            source_realm_id: Some(2),
327            from_type: CHAT_ACTOR_TYPE.to_string(),
328            to_type: CHAT_ACTOR_TYPE.to_string(),
329            access: true,
330        },
331        ActorAcl {
332            rowid: None,
333            realm_id: 2,
334            source_realm_id: Some(2),
335            from_type: VOICE_ACTOR_TYPE.to_string(),
336            to_type: ANONYMOUS_ACTOR_TYPE.to_string(),
337            access: true,
338        },
339        ActorAcl {
340            rowid: None,
341            realm_id: 2,
342            source_realm_id: Some(2),
343            from_type: VOICE_ACTOR_TYPE.to_string(),
344            to_type: CHAT_ACTOR_TYPE.to_string(),
345            access: true,
346        },
347        ActorAcl {
348            rowid: None,
349            realm_id: 2,
350            source_realm_id: Some(2),
351            from_type: VOICE_ACTOR_TYPE.to_string(),
352            to_type: VOICE_ACTOR_TYPE.to_string(),
353            access: true,
354        },
355    ]
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use crate::realm::Realm;
362    use crate::util::test_utils::utils::setup_test_db;
363    use serial_test::serial;
364
365    #[tokio::test]
366    #[serial]
367    async fn test_actor_acl_crud() -> anyhow::Result<()> {
368        setup_test_db().await?;
369
370        // Create a realm first
371        let realm = Realm::create("acl_test_realm".to_string(), String::new()).await?;
372        let realm_id = realm.id;
373
374        // Test create
375        let mut acl = ActorAcl::new(
376            realm_id,
377            "identified_client_user".to_string(),
378            "identified_client_room".to_string(),
379            true,
380        );
381        let acl_id = acl.save().await?;
382        assert!(acl.rowid.is_some());
383        assert_eq!(acl.rowid.unwrap(), acl_id);
384
385        // Test get
386        let fetched_opt = ActorAcl::get(acl_id).await?;
387        assert!(fetched_opt.is_some());
388        let fetched = fetched_opt.unwrap();
389        assert_eq!(fetched.realm_id, realm_id);
390        assert_eq!(fetched.source_realm_id, Some(realm_id));
391        assert_eq!(fetched.from_type, "identified_client_user");
392        assert_eq!(fetched.to_type, "identified_client_room");
393        assert!(fetched.access);
394
395        // Test update
396        acl.access = false;
397        let updated_acl_id = acl.save().await?;
398        assert_eq!(updated_acl_id, acl_id);
399
400        let reloaded_opt = ActorAcl::get(acl_id).await?;
401        assert!(reloaded_opt.is_some());
402        let reloaded = reloaded_opt.unwrap();
403        assert!(!reloaded.access);
404
405        // Test get_by_realm
406        let acls_for_realm = ActorAcl::get_by_realm(realm_id).await?;
407        assert_eq!(acls_for_realm.len(), 1);
408        assert_eq!(acls_for_realm[0].rowid, Some(acl_id));
409
410        // Test get_by_types
411        let acl_by_types_opt = ActorAcl::get_by_types(
412            realm_id,
413            realm_id,
414            "identified_client_user",
415            "identified_client_room",
416        )
417        .await?;
418        assert!(acl_by_types_opt.is_some());
419        let acl_by_types = acl_by_types_opt.unwrap();
420        assert!(!acl_by_types.access);
421
422        // Test delete
423        ActorAcl::delete_by_id(acl_id).await?;
424        let deleted_acl_opt = ActorAcl::get(acl_id).await?;
425        assert!(deleted_acl_opt.is_none());
426
427        Ok(())
428    }
429
430    #[tokio::test]
431    #[serial]
432    async fn test_cross_realm_acl_allow_and_deny() -> anyhow::Result<()> {
433        setup_test_db().await?;
434
435        let source = Realm::create("acl_source_realm".to_string(), String::new()).await?;
436        let target = Realm::create("acl_target_realm".to_string(), String::new()).await?;
437        let source_realm = source.id;
438        let target_realm = target.id;
439
440        let mut acl = ActorAcl::new_with_source_realm(
441            target_realm,
442            Some(source_realm),
443            "acme:client".to_string(),
444            "acme:worker".to_string(),
445            true,
446        );
447        let _ = acl.save().await?;
448
449        let allowed =
450            ActorAcl::can_discover(source_realm, target_realm, "acme:client", "acme:worker")
451                .await?;
452        assert!(allowed);
453
454        let denied_same_realm =
455            ActorAcl::can_discover(target_realm, target_realm, "acme:client", "acme:worker")
456                .await?;
457        assert!(!denied_same_realm);
458
459        let denied_other_type =
460            ActorAcl::can_discover(source_realm, target_realm, "acme:other", "acme:worker").await?;
461        assert!(!denied_other_type);
462
463        Ok(())
464    }
465}