Skip to main content

pylon_plugin/builtin/
organizations.rs

1use std::collections::HashMap;
2use std::sync::Mutex;
3
4use crate::Plugin;
5
6/// Member role within an organization.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum OrgRole {
9    Owner,
10    Admin,
11    Member,
12}
13
14impl OrgRole {
15    pub fn from_str(s: &str) -> Option<Self> {
16        match s {
17            "owner" => Some(Self::Owner),
18            "admin" => Some(Self::Admin),
19            "member" => Some(Self::Member),
20            _ => None,
21        }
22    }
23
24    pub fn as_str(&self) -> &str {
25        match self {
26            Self::Owner => "owner",
27            Self::Admin => "admin",
28            Self::Member => "member",
29        }
30    }
31
32    pub fn can_manage_members(&self) -> bool {
33        matches!(self, Self::Owner | Self::Admin)
34    }
35
36    pub fn can_delete_org(&self) -> bool {
37        matches!(self, Self::Owner)
38    }
39}
40
41/// An organization.
42#[derive(Debug, Clone)]
43pub struct Organization {
44    pub id: String,
45    pub name: String,
46    pub created_by: String,
47    pub created_at: String,
48}
49
50/// A membership in an organization.
51#[derive(Debug, Clone)]
52pub struct Membership {
53    pub org_id: String,
54    pub user_id: String,
55    pub role: OrgRole,
56    pub joined_at: String,
57}
58
59/// Organizations plugin. Multi-tenant team management with roles.
60pub struct OrganizationsPlugin {
61    orgs: Mutex<HashMap<String, Organization>>,
62    members: Mutex<Vec<Membership>>,
63    next_id: Mutex<u64>,
64}
65
66impl OrganizationsPlugin {
67    pub fn new() -> Self {
68        Self {
69            orgs: Mutex::new(HashMap::new()),
70            members: Mutex::new(Vec::new()),
71            next_id: Mutex::new(0),
72        }
73    }
74
75    /// Create a new organization. The creator becomes the owner.
76    pub fn create_org(&self, name: &str, creator_id: &str) -> Organization {
77        let mut id_counter = self.next_id.lock().unwrap();
78        *id_counter += 1;
79        let id = format!("org_{}", *id_counter);
80
81        let org = Organization {
82            id: id.clone(),
83            name: name.to_string(),
84            created_by: creator_id.to_string(),
85            created_at: now(),
86        };
87
88        self.orgs.lock().unwrap().insert(id.clone(), org.clone());
89
90        // Add creator as owner.
91        self.members.lock().unwrap().push(Membership {
92            org_id: id,
93            user_id: creator_id.to_string(),
94            role: OrgRole::Owner,
95            joined_at: now(),
96        });
97
98        org
99    }
100
101    /// Add a member to an organization.
102    pub fn add_member(&self, org_id: &str, user_id: &str, role: OrgRole) -> Result<(), String> {
103        if !self.orgs.lock().unwrap().contains_key(org_id) {
104            return Err(format!("Organization {} not found", org_id));
105        }
106
107        let mut members = self.members.lock().unwrap();
108        if members
109            .iter()
110            .any(|m| m.org_id == org_id && m.user_id == user_id)
111        {
112            return Err("User is already a member".into());
113        }
114
115        members.push(Membership {
116            org_id: org_id.to_string(),
117            user_id: user_id.to_string(),
118            role,
119            joined_at: now(),
120        });
121        Ok(())
122    }
123
124    /// Remove a member from an organization.
125    pub fn remove_member(&self, org_id: &str, user_id: &str) -> bool {
126        let mut members = self.members.lock().unwrap();
127        let before = members.len();
128        members.retain(|m| !(m.org_id == org_id && m.user_id == user_id));
129        members.len() < before
130    }
131
132    /// Get a user's role in an organization.
133    pub fn get_role(&self, org_id: &str, user_id: &str) -> Option<OrgRole> {
134        self.members
135            .lock()
136            .unwrap()
137            .iter()
138            .find(|m| m.org_id == org_id && m.user_id == user_id)
139            .map(|m| m.role.clone())
140    }
141
142    /// List all members of an organization.
143    pub fn list_members(&self, org_id: &str) -> Vec<Membership> {
144        self.members
145            .lock()
146            .unwrap()
147            .iter()
148            .filter(|m| m.org_id == org_id)
149            .cloned()
150            .collect()
151    }
152
153    /// List all organizations a user belongs to.
154    pub fn list_user_orgs(&self, user_id: &str) -> Vec<(Organization, OrgRole)> {
155        let members = self.members.lock().unwrap();
156        let orgs = self.orgs.lock().unwrap();
157
158        members
159            .iter()
160            .filter(|m| m.user_id == user_id)
161            .filter_map(|m| orgs.get(&m.org_id).map(|o| (o.clone(), m.role.clone())))
162            .collect()
163    }
164
165    /// Check if a user is a member of an organization.
166    pub fn is_member(&self, org_id: &str, user_id: &str) -> bool {
167        self.get_role(org_id, user_id).is_some()
168    }
169
170    /// Delete an organization and all its memberships.
171    pub fn delete_org(&self, org_id: &str) -> bool {
172        let removed = self.orgs.lock().unwrap().remove(org_id).is_some();
173        if removed {
174            self.members.lock().unwrap().retain(|m| m.org_id != org_id);
175        }
176        removed
177    }
178}
179
180impl Plugin for OrganizationsPlugin {
181    fn name(&self) -> &str {
182        "organizations"
183    }
184}
185
186fn now() -> String {
187    let ts = SystemTime::now()
188        .duration_since(std::time::UNIX_EPOCH)
189        .unwrap_or_default()
190        .as_secs();
191    format!("{ts}Z")
192}
193
194use std::time::SystemTime;
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn create_org() {
202        let plugin = OrganizationsPlugin::new();
203        let org = plugin.create_org("My Team", "user-1");
204        assert!(!org.id.is_empty());
205        assert_eq!(org.name, "My Team");
206        assert_eq!(org.created_by, "user-1");
207    }
208
209    #[test]
210    fn creator_is_owner() {
211        let plugin = OrganizationsPlugin::new();
212        let org = plugin.create_org("Team", "user-1");
213        let role = plugin.get_role(&org.id, "user-1").unwrap();
214        assert_eq!(role, OrgRole::Owner);
215    }
216
217    #[test]
218    fn add_and_list_members() {
219        let plugin = OrganizationsPlugin::new();
220        let org = plugin.create_org("Team", "user-1");
221        plugin
222            .add_member(&org.id, "user-2", OrgRole::Admin)
223            .unwrap();
224        plugin
225            .add_member(&org.id, "user-3", OrgRole::Member)
226            .unwrap();
227
228        let members = plugin.list_members(&org.id);
229        assert_eq!(members.len(), 3);
230    }
231
232    #[test]
233    fn duplicate_member_rejected() {
234        let plugin = OrganizationsPlugin::new();
235        let org = plugin.create_org("Team", "user-1");
236        let result = plugin.add_member(&org.id, "user-1", OrgRole::Member);
237        assert!(result.is_err());
238    }
239
240    #[test]
241    fn remove_member() {
242        let plugin = OrganizationsPlugin::new();
243        let org = plugin.create_org("Team", "user-1");
244        plugin
245            .add_member(&org.id, "user-2", OrgRole::Member)
246            .unwrap();
247
248        assert!(plugin.remove_member(&org.id, "user-2"));
249        assert!(!plugin.is_member(&org.id, "user-2"));
250    }
251
252    #[test]
253    fn list_user_orgs() {
254        let plugin = OrganizationsPlugin::new();
255        let org1 = plugin.create_org("Team A", "user-1");
256        let org2 = plugin.create_org("Team B", "user-2");
257        plugin
258            .add_member(&org2.id, "user-1", OrgRole::Member)
259            .unwrap();
260
261        let orgs = plugin.list_user_orgs("user-1");
262        assert_eq!(orgs.len(), 2);
263    }
264
265    #[test]
266    fn role_permissions() {
267        assert!(OrgRole::Owner.can_manage_members());
268        assert!(OrgRole::Owner.can_delete_org());
269        assert!(OrgRole::Admin.can_manage_members());
270        assert!(!OrgRole::Admin.can_delete_org());
271        assert!(!OrgRole::Member.can_manage_members());
272        assert!(!OrgRole::Member.can_delete_org());
273    }
274
275    #[test]
276    fn delete_org() {
277        let plugin = OrganizationsPlugin::new();
278        let org = plugin.create_org("Team", "user-1");
279        plugin
280            .add_member(&org.id, "user-2", OrgRole::Member)
281            .unwrap();
282
283        assert!(plugin.delete_org(&org.id));
284        assert!(plugin.list_members(&org.id).is_empty());
285        assert!(!plugin.is_member(&org.id, "user-1"));
286    }
287
288    #[test]
289    fn add_to_nonexistent_org() {
290        let plugin = OrganizationsPlugin::new();
291        let result = plugin.add_member("org_999", "user-1", OrgRole::Member);
292        assert!(result.is_err());
293    }
294}