guts_auth/
team.rs

1//! Team types for group-based repository access.
2
3use crate::permission::Permission;
4use serde::{Deserialize, Serialize};
5use std::collections::HashSet;
6
7/// A team within an organization.
8///
9/// Teams allow grouping users for easier permission management.
10/// A team can have access to multiple repositories with a specific permission level.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Team {
13    /// Unique team ID.
14    pub id: u64,
15    /// Organization ID this team belongs to.
16    pub org_id: u64,
17    /// Team name (URL-safe, unique within org).
18    pub name: String,
19    /// Optional description.
20    pub description: Option<String>,
21    /// Team members (public keys).
22    pub members: HashSet<String>,
23    /// Default permission level for team repositories.
24    pub permission: Permission,
25    /// Repository keys this team has access to.
26    pub repos: HashSet<String>,
27    /// When the team was created (Unix timestamp).
28    pub created_at: u64,
29    /// When the team was last updated (Unix timestamp).
30    pub updated_at: u64,
31    /// Who created this team (public key).
32    pub created_by: String,
33}
34
35impl Team {
36    /// Create a new team.
37    pub fn new(
38        id: u64,
39        org_id: u64,
40        name: String,
41        permission: Permission,
42        created_by: String,
43    ) -> Self {
44        let now = Self::now();
45        Self {
46            id,
47            org_id,
48            name,
49            description: None,
50            members: HashSet::new(),
51            permission,
52            repos: HashSet::new(),
53            created_at: now,
54            updated_at: now,
55            created_by,
56        }
57    }
58
59    /// Set the team description.
60    pub fn with_description(mut self, description: impl Into<String>) -> Self {
61        self.description = Some(description.into());
62        self.updated_at = Self::now();
63        self
64    }
65
66    /// Check if a user is a member of this team.
67    pub fn is_member(&self, user: &str) -> bool {
68        self.members.contains(user)
69    }
70
71    /// Add a member to the team.
72    pub fn add_member(&mut self, user: String) -> bool {
73        let added = self.members.insert(user);
74        if added {
75            self.updated_at = Self::now();
76        }
77        added
78    }
79
80    /// Remove a member from the team.
81    pub fn remove_member(&mut self, user: &str) -> bool {
82        let removed = self.members.remove(user);
83        if removed {
84            self.updated_at = Self::now();
85        }
86        removed
87    }
88
89    /// Check if the team has access to a repository.
90    pub fn has_repo(&self, repo_key: &str) -> bool {
91        self.repos.contains(repo_key)
92    }
93
94    /// Add a repository to the team.
95    pub fn add_repo(&mut self, repo_key: String) -> bool {
96        let added = self.repos.insert(repo_key);
97        if added {
98            self.updated_at = Self::now();
99        }
100        added
101    }
102
103    /// Remove a repository from the team.
104    pub fn remove_repo(&mut self, repo_key: &str) -> bool {
105        let removed = self.repos.remove(repo_key);
106        if removed {
107            self.updated_at = Self::now();
108        }
109        removed
110    }
111
112    /// Get the permission level for a specific repository.
113    /// Returns the team's permission if the repo is in the team, None otherwise.
114    pub fn get_repo_permission(&self, repo_key: &str) -> Option<Permission> {
115        if self.repos.contains(repo_key) {
116            Some(self.permission)
117        } else {
118            None
119        }
120    }
121
122    /// Update the team's default permission level.
123    pub fn set_permission(&mut self, permission: Permission) {
124        self.permission = permission;
125        self.updated_at = Self::now();
126    }
127
128    fn now() -> u64 {
129        std::time::SystemTime::now()
130            .duration_since(std::time::UNIX_EPOCH)
131            .unwrap_or_default()
132            .as_secs()
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_team_creation() {
142        let team = Team::new(1, 1, "backend".into(), Permission::Write, "creator".into());
143
144        assert_eq!(team.id, 1);
145        assert_eq!(team.org_id, 1);
146        assert_eq!(team.name, "backend");
147        assert_eq!(team.permission, Permission::Write);
148        assert!(team.members.is_empty());
149        assert!(team.repos.is_empty());
150    }
151
152    #[test]
153    fn test_team_members() {
154        let mut team = Team::new(1, 1, "backend".into(), Permission::Write, "creator".into());
155
156        assert!(team.add_member("user1".into()));
157        assert!(team.is_member("user1"));
158        assert!(!team.add_member("user1".into())); // Already exists
159
160        assert!(team.add_member("user2".into()));
161        assert_eq!(team.members.len(), 2);
162
163        assert!(team.remove_member("user1"));
164        assert!(!team.is_member("user1"));
165        assert!(!team.remove_member("user1")); // Already removed
166    }
167
168    #[test]
169    fn test_team_repos() {
170        let mut team = Team::new(1, 1, "backend".into(), Permission::Write, "creator".into());
171
172        assert!(team.add_repo("acme/api".into()));
173        assert!(team.has_repo("acme/api"));
174        assert_eq!(
175            team.get_repo_permission("acme/api"),
176            Some(Permission::Write)
177        );
178        assert_eq!(team.get_repo_permission("acme/other"), None);
179
180        assert!(team.remove_repo("acme/api"));
181        assert!(!team.has_repo("acme/api"));
182    }
183
184    #[test]
185    fn test_team_permission_update() {
186        let mut team = Team::new(1, 1, "backend".into(), Permission::Read, "creator".into());
187        team.add_repo("acme/api".into());
188
189        assert_eq!(team.get_repo_permission("acme/api"), Some(Permission::Read));
190
191        team.set_permission(Permission::Admin);
192        assert_eq!(
193            team.get_repo_permission("acme/api"),
194            Some(Permission::Admin)
195        );
196    }
197}