Skip to main content

miryad_core/users/
group.rs

1use chrono::Utc;
2use sea_orm::entity::prelude::*;
3use sea_orm::{ConnectionTrait, Set};
4
5use crate::users::membership;
6
7/// Nom du groupe admin, pré-câblé (seedé par migration) mais sans rien de spécial au niveau
8/// schéma — juste la convention lue par l'évaluateur RBAC (`rbac::is_admin`).
9pub const ADMIN_GROUP_NAME: &str = "admin";
10
11#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
12#[sea_orm(table_name = "miryad_groups")]
13pub struct Model {
14    #[sea_orm(primary_key)]
15    pub id: i32,
16    #[sea_orm(unique)]
17    pub name: String,
18    pub created_at: DateTimeUtc,
19}
20
21#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
22pub enum Relation {}
23
24impl ActiveModelBehavior for ActiveModel {}
25
26pub type Group = Entity;
27
28/// Get-or-create par nom — un groupe cité dans un claim `groups` mais jamais vu est créé à la
29/// volée (pas de registre préalable des noms de groupe autorisés).
30pub async fn ensure_group<C: ConnectionTrait>(db: &C, name: &str) -> Result<i32, DbErr> {
31    if let Some(existing) = Entity::find().filter(Column::Name.eq(name)).one(db).await? {
32        return Ok(existing.id);
33    }
34
35    let active = ActiveModel {
36        name: Set(name.to_string()),
37        created_at: Set(Utc::now()),
38        ..Default::default()
39    };
40
41    match active.insert(db).await {
42        Ok(model) => Ok(model.id),
43        Err(_) => Entity::find()
44            .filter(Column::Name.eq(name))
45            .one(db)
46            .await?
47            .map(|g| g.id)
48            .ok_or_else(|| DbErr::RecordNotFound(format!("group {name} vanished"))),
49    }
50}
51
52pub async fn is_admin<C: ConnectionTrait>(db: &C, user_id: i32) -> Result<bool, DbErr> {
53    is_member(db, user_id, ADMIN_GROUP_NAME).await
54}
55
56pub async fn is_member<C: ConnectionTrait>(db: &C, user_id: i32, group_name: &str) -> Result<bool, DbErr> {
57    let Some(group) = Entity::find().filter(Column::Name.eq(group_name)).one(db).await? else {
58        return Ok(false);
59    };
60
61    let exists = membership::Entity::find()
62        .filter(membership::Column::UserId.eq(user_id))
63        .filter(membership::Column::GroupId.eq(group.id))
64        .one(db)
65        .await?
66        .is_some();
67    Ok(exists)
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use crate::migration::Migrator;
74    use crate::users::membership::sync_group_memberships;
75    use crate::users::user::resolve_user;
76    use sea_orm_migration::MigratorTrait;
77
78    async fn test_db() -> DatabaseConnection {
79        let db = sea_orm::Database::connect("sqlite::memory:")
80            .await
81            .expect("in-memory sqlite connects");
82        Migrator::up(&db, None).await.expect("migrations apply cleanly");
83        db
84    }
85
86    #[tokio::test]
87    async fn admin_group_is_seeded_by_migration() {
88        let db = test_db().await;
89        let admin_group = Entity::find()
90            .filter(Column::Name.eq(ADMIN_GROUP_NAME))
91            .one(&db)
92            .await
93            .expect("query succeeds");
94        assert!(admin_group.is_some());
95    }
96
97    #[tokio::test]
98    async fn is_member_true_for_member_false_otherwise() {
99        let db = test_db().await;
100        let user = resolve_user(&db, "sub-1", None).await.expect("resolve succeeds");
101        sync_group_memberships(&db, user.id, &["editors".to_string()])
102            .await
103            .expect("sync succeeds");
104
105        assert!(is_member(&db, user.id, "editors").await.expect("query succeeds"));
106        assert!(!is_member(&db, user.id, "admin").await.expect("query succeeds"));
107    }
108
109    #[tokio::test]
110    async fn is_member_false_for_unknown_group() {
111        let db = test_db().await;
112        let user = resolve_user(&db, "sub-1", None).await.expect("resolve succeeds");
113        assert!(
114            !is_member(&db, user.id, "does-not-exist")
115                .await
116                .expect("query succeeds")
117        );
118    }
119}