1use crate::{
4 branch_protection::BranchProtection,
5 collaborator::Collaborator,
6 error::{AuthError, Result},
7 organization::{OrgMember, OrgRole, Organization},
8 permission::Permission,
9 team::Team,
10 webhook::Webhook,
11};
12use parking_lot::RwLock;
13use std::collections::HashMap;
14use std::sync::atomic::{AtomicU64, Ordering};
15
16#[derive(Debug, Default)]
18pub struct AuthStore {
19 next_id: AtomicU64,
21
22 organizations: RwLock<HashMap<u64, Organization>>,
24
25 org_name_index: RwLock<HashMap<String, u64>>,
27
28 teams: RwLock<HashMap<u64, Team>>,
30
31 collaborators: RwLock<HashMap<(String, String), Collaborator>>,
33
34 collaborator_index: RwLock<HashMap<String, Vec<(String, Permission)>>>,
36
37 branch_protections: RwLock<HashMap<(String, String), BranchProtection>>,
39
40 webhooks: RwLock<HashMap<u64, Webhook>>,
42
43 webhook_index: RwLock<HashMap<String, Vec<u64>>>,
45}
46
47impl AuthStore {
48 pub fn new() -> Self {
50 Self::default()
51 }
52
53 fn next_id(&self) -> u64 {
55 self.next_id.fetch_add(1, Ordering::SeqCst) + 1
56 }
57
58 pub fn create_organization(
62 &self,
63 name: String,
64 display_name: String,
65 creator: String,
66 ) -> Result<Organization> {
67 if self.org_name_index.read().contains_key(&name) {
69 return Err(AuthError::AlreadyExists(format!("organization '{}'", name)));
70 }
71
72 let id = self.next_id();
73 let org = Organization::new(id, name.clone(), display_name, creator);
74
75 self.organizations.write().insert(id, org.clone());
76 self.org_name_index.write().insert(name, id);
77
78 Ok(org)
79 }
80
81 pub fn get_organization(&self, id: u64) -> Option<Organization> {
83 self.organizations.read().get(&id).cloned()
84 }
85
86 pub fn get_organization_by_name(&self, name: &str) -> Option<Organization> {
88 let id = self.org_name_index.read().get(name).copied()?;
89 self.get_organization(id)
90 }
91
92 pub fn list_organizations(&self) -> Vec<Organization> {
94 self.organizations.read().values().cloned().collect()
95 }
96
97 pub fn list_user_organizations(&self, user: &str) -> Vec<Organization> {
99 self.organizations
100 .read()
101 .values()
102 .filter(|org| org.is_member(user))
103 .cloned()
104 .collect()
105 }
106
107 pub fn update_organization(
109 &self,
110 id: u64,
111 display_name: Option<String>,
112 description: Option<String>,
113 ) -> Result<Organization> {
114 let mut orgs = self.organizations.write();
115 let org = orgs
116 .get_mut(&id)
117 .ok_or_else(|| AuthError::NotFound(format!("organization {}", id)))?;
118
119 if let Some(dn) = display_name {
120 org.display_name = dn;
121 }
122 if let Some(desc) = description {
123 org.description = Some(desc);
124 }
125 org.updated_at = Self::now();
126
127 Ok(org.clone())
128 }
129
130 pub fn delete_organization(&self, id: u64) -> Result<()> {
132 let mut orgs = self.organizations.write();
133 let org = orgs
134 .remove(&id)
135 .ok_or_else(|| AuthError::NotFound(format!("organization {}", id)))?;
136
137 self.org_name_index.write().remove(&org.name);
139
140 let team_ids: Vec<u64> = org.teams.iter().copied().collect();
142 for team_id in team_ids {
143 self.teams.write().remove(&team_id);
144 }
145
146 Ok(())
147 }
148
149 pub fn add_org_member(&self, org_id: u64, member: OrgMember) -> Result<()> {
151 let mut orgs = self.organizations.write();
152 let org = orgs
153 .get_mut(&org_id)
154 .ok_or_else(|| AuthError::NotFound(format!("organization {}", org_id)))?;
155
156 if !org.add_member(member.clone()) {
157 return Err(AuthError::AlreadyExists(format!(
158 "member '{}'",
159 member.user
160 )));
161 }
162
163 Ok(())
164 }
165
166 pub fn remove_org_member(&self, org_id: u64, user: &str) -> Result<()> {
168 let mut orgs = self.organizations.write();
169 let org = orgs
170 .get_mut(&org_id)
171 .ok_or_else(|| AuthError::NotFound(format!("organization {}", org_id)))?;
172
173 org.remove_member(user).map_err(|_| AuthError::LastOwner)?;
174
175 Ok(())
176 }
177
178 pub fn update_org_member_role(&self, org_id: u64, user: &str, role: OrgRole) -> Result<()> {
180 let mut orgs = self.organizations.write();
181 let org = orgs
182 .get_mut(&org_id)
183 .ok_or_else(|| AuthError::NotFound(format!("organization {}", org_id)))?;
184
185 org.update_member_role(user, role)
186 .map_err(|_| AuthError::LastOwner)?;
187
188 Ok(())
189 }
190
191 pub fn create_team(
195 &self,
196 org_id: u64,
197 name: String,
198 permission: Permission,
199 created_by: String,
200 ) -> Result<Team> {
201 let mut orgs = self.organizations.write();
203 let org = orgs
204 .get_mut(&org_id)
205 .ok_or_else(|| AuthError::NotFound(format!("organization {}", org_id)))?;
206
207 let teams = self.teams.read();
209 if teams.values().any(|t| t.org_id == org_id && t.name == name) {
210 return Err(AuthError::AlreadyExists(format!(
211 "team '{}' in organization",
212 name
213 )));
214 }
215 drop(teams);
216
217 let id = self.next_id();
218 let team = Team::new(id, org_id, name, permission, created_by);
219
220 org.add_team(id);
221 self.teams.write().insert(id, team.clone());
222
223 Ok(team)
224 }
225
226 pub fn get_team(&self, id: u64) -> Option<Team> {
228 self.teams.read().get(&id).cloned()
229 }
230
231 pub fn get_team_by_name(&self, org_id: u64, name: &str) -> Option<Team> {
233 self.teams
234 .read()
235 .values()
236 .find(|t| t.org_id == org_id && t.name == name)
237 .cloned()
238 }
239
240 pub fn list_teams(&self, org_id: u64) -> Vec<Team> {
242 self.teams
243 .read()
244 .values()
245 .filter(|t| t.org_id == org_id)
246 .cloned()
247 .collect()
248 }
249
250 pub fn list_user_teams(&self, user: &str) -> Vec<Team> {
252 self.teams
253 .read()
254 .values()
255 .filter(|t| t.is_member(user))
256 .cloned()
257 .collect()
258 }
259
260 pub fn update_team(
262 &self,
263 id: u64,
264 name: Option<String>,
265 description: Option<String>,
266 permission: Option<Permission>,
267 ) -> Result<Team> {
268 let mut teams = self.teams.write();
269 let team = teams
270 .get_mut(&id)
271 .ok_or_else(|| AuthError::NotFound(format!("team {}", id)))?;
272
273 if let Some(n) = name {
274 team.name = n;
275 }
276 if let Some(d) = description {
277 team.description = Some(d);
278 }
279 if let Some(p) = permission {
280 team.permission = p;
281 }
282 team.updated_at = Self::now();
283
284 Ok(team.clone())
285 }
286
287 pub fn delete_team(&self, id: u64) -> Result<()> {
289 let team = self
290 .teams
291 .write()
292 .remove(&id)
293 .ok_or_else(|| AuthError::NotFound(format!("team {}", id)))?;
294
295 if let Some(org) = self.organizations.write().get_mut(&team.org_id) {
297 org.remove_team(id);
298 }
299
300 Ok(())
301 }
302
303 pub fn add_team_member(&self, team_id: u64, user: String) -> Result<()> {
305 let mut teams = self.teams.write();
306 let team = teams
307 .get_mut(&team_id)
308 .ok_or_else(|| AuthError::NotFound(format!("team {}", team_id)))?;
309
310 if !team.add_member(user.clone()) {
311 return Err(AuthError::AlreadyExists(format!(
312 "member '{}' in team",
313 user
314 )));
315 }
316
317 Ok(())
318 }
319
320 pub fn remove_team_member(&self, team_id: u64, user: &str) -> Result<()> {
322 let mut teams = self.teams.write();
323 let team = teams
324 .get_mut(&team_id)
325 .ok_or_else(|| AuthError::NotFound(format!("team {}", team_id)))?;
326
327 if !team.remove_member(user) {
328 return Err(AuthError::NotFound(format!("member '{}' in team", user)));
329 }
330
331 Ok(())
332 }
333
334 pub fn add_team_repo(&self, team_id: u64, repo_key: String) -> Result<()> {
336 let mut teams = self.teams.write();
337 let team = teams
338 .get_mut(&team_id)
339 .ok_or_else(|| AuthError::NotFound(format!("team {}", team_id)))?;
340
341 team.add_repo(repo_key);
342 Ok(())
343 }
344
345 pub fn remove_team_repo(&self, team_id: u64, repo_key: &str) -> Result<()> {
347 let mut teams = self.teams.write();
348 let team = teams
349 .get_mut(&team_id)
350 .ok_or_else(|| AuthError::NotFound(format!("team {}", team_id)))?;
351
352 if !team.remove_repo(repo_key) {
353 return Err(AuthError::NotFound(format!("repo '{}' in team", repo_key)));
354 }
355
356 Ok(())
357 }
358
359 pub fn set_collaborator(
363 &self,
364 repo_key: String,
365 user: String,
366 permission: Permission,
367 added_by: String,
368 ) -> Collaborator {
369 let key = (repo_key.clone(), user.clone());
370 let mut collabs = self.collaborators.write();
371
372 if let Some(existing) = collabs.get_mut(&key) {
373 existing.permission = permission;
374 existing.updated_at = Self::now();
375 return existing.clone();
376 }
377
378 let id = self.next_id();
379 let collab = Collaborator::new(id, repo_key.clone(), user.clone(), permission, added_by);
380 collabs.insert(key, collab.clone());
381
382 let mut index = self.collaborator_index.write();
384 index.entry(repo_key).or_default().push((user, permission));
385
386 collab
387 }
388
389 pub fn get_collaborator(&self, repo_key: &str, user: &str) -> Option<Collaborator> {
391 let key = (repo_key.to_string(), user.to_string());
392 self.collaborators.read().get(&key).cloned()
393 }
394
395 pub fn list_collaborators(&self, repo_key: &str) -> Vec<Collaborator> {
397 self.collaborators
398 .read()
399 .values()
400 .filter(|c| c.repo_key == repo_key)
401 .cloned()
402 .collect()
403 }
404
405 pub fn remove_collaborator(&self, repo_key: &str, user: &str) -> Result<()> {
407 let key = (repo_key.to_string(), user.to_string());
408 if self.collaborators.write().remove(&key).is_none() {
409 return Err(AuthError::NotFound(format!(
410 "collaborator '{}' on '{}'",
411 user, repo_key
412 )));
413 }
414
415 let mut index = self.collaborator_index.write();
417 if let Some(users) = index.get_mut(repo_key) {
418 users.retain(|(u, _)| u != user);
419 }
420
421 Ok(())
422 }
423
424 pub fn set_branch_protection(&self, repo_key: String, pattern: String) -> BranchProtection {
428 let key = (repo_key.clone(), pattern.clone());
429 let mut protections = self.branch_protections.write();
430
431 if let Some(existing) = protections.get(&key) {
432 return existing.clone();
433 }
434
435 let id = self.next_id();
436 let protection = BranchProtection::new(id, repo_key, pattern);
437 protections.insert(key, protection.clone());
438 protection
439 }
440
441 pub fn get_branch_protection(&self, repo_key: &str, pattern: &str) -> Option<BranchProtection> {
443 let key = (repo_key.to_string(), pattern.to_string());
444 self.branch_protections.read().get(&key).cloned()
445 }
446
447 pub fn find_branch_protection(&self, repo_key: &str, branch: &str) -> Option<BranchProtection> {
449 self.branch_protections
450 .read()
451 .values()
452 .filter(|p| p.repo_key == repo_key && p.matches(branch))
453 .max_by_key(|p| p.pattern.len()) .cloned()
455 }
456
457 pub fn list_branch_protections(&self, repo_key: &str) -> Vec<BranchProtection> {
459 self.branch_protections
460 .read()
461 .values()
462 .filter(|p| p.repo_key == repo_key)
463 .cloned()
464 .collect()
465 }
466
467 pub fn update_branch_protection(
469 &self,
470 repo_key: &str,
471 pattern: &str,
472 update: impl FnOnce(&mut BranchProtection),
473 ) -> Result<BranchProtection> {
474 let key = (repo_key.to_string(), pattern.to_string());
475 let mut protections = self.branch_protections.write();
476 let protection = protections
477 .get_mut(&key)
478 .ok_or_else(|| AuthError::NotFound(format!("branch protection for '{}'", pattern)))?;
479
480 update(protection);
481 protection.updated_at = Self::now();
482
483 Ok(protection.clone())
484 }
485
486 pub fn remove_branch_protection(&self, repo_key: &str, pattern: &str) -> Result<()> {
488 let key = (repo_key.to_string(), pattern.to_string());
489 if self.branch_protections.write().remove(&key).is_none() {
490 return Err(AuthError::NotFound(format!(
491 "branch protection for '{}'",
492 pattern
493 )));
494 }
495 Ok(())
496 }
497
498 pub fn create_webhook(
502 &self,
503 repo_key: String,
504 url: String,
505 events: std::collections::HashSet<crate::webhook::WebhookEvent>,
506 ) -> Webhook {
507 let id = self.next_id();
508 let webhook = Webhook::new(id, repo_key.clone(), url, events);
509
510 self.webhooks.write().insert(id, webhook.clone());
511
512 self.webhook_index
514 .write()
515 .entry(repo_key)
516 .or_default()
517 .push(id);
518
519 webhook
520 }
521
522 pub fn get_webhook(&self, id: u64) -> Option<Webhook> {
524 self.webhooks.read().get(&id).cloned()
525 }
526
527 pub fn list_webhooks(&self, repo_key: &str) -> Vec<Webhook> {
529 self.webhooks
530 .read()
531 .values()
532 .filter(|w| w.repo_key == repo_key)
533 .cloned()
534 .collect()
535 }
536
537 pub fn update_webhook(&self, id: u64, update: impl FnOnce(&mut Webhook)) -> Result<Webhook> {
539 let mut webhooks = self.webhooks.write();
540 let webhook = webhooks
541 .get_mut(&id)
542 .ok_or_else(|| AuthError::NotFound(format!("webhook {}", id)))?;
543
544 update(webhook);
545 webhook.updated_at = Self::now();
546
547 Ok(webhook.clone())
548 }
549
550 pub fn delete_webhook(&self, id: u64) -> Result<()> {
552 let webhook = self
553 .webhooks
554 .write()
555 .remove(&id)
556 .ok_or_else(|| AuthError::NotFound(format!("webhook {}", id)))?;
557
558 if let Some(ids) = self.webhook_index.write().get_mut(&webhook.repo_key) {
560 ids.retain(|&wid| wid != id);
561 }
562
563 Ok(())
564 }
565
566 pub fn find_webhooks_for_event(
568 &self,
569 repo_key: &str,
570 event: crate::webhook::WebhookEvent,
571 ) -> Vec<Webhook> {
572 self.webhooks
573 .read()
574 .values()
575 .filter(|w| w.repo_key == repo_key && w.should_fire(event))
576 .cloned()
577 .collect()
578 }
579
580 pub fn check_permission(&self, user: &str, repo_key: &str, required: Permission) -> bool {
590 if let Some((owner, _)) = repo_key.split_once('/') {
592 if owner == user {
593 return true; }
595
596 if let Some(org) = self.get_organization_by_name(owner) {
598 if org.is_admin(user) {
600 return true;
601 }
602 if org.is_member(user) && required == Permission::Read {
604 return true;
605 }
606 }
607 }
608
609 if let Some(collab) = self.get_collaborator(repo_key, user) {
611 if collab.has_permission(required) {
612 return true;
613 }
614 }
615
616 let user_teams = self.list_user_teams(user);
618 for team in user_teams {
619 if let Some(perm) = team.get_repo_permission(repo_key) {
620 if perm.has(required) {
621 return true;
622 }
623 }
624 }
625
626 false
627 }
628
629 pub fn get_effective_permission(&self, user: &str, repo_key: &str) -> Option<Permission> {
631 let mut best: Option<Permission> = None;
632
633 if let Some((owner, _)) = repo_key.split_once('/') {
635 if owner == user {
636 return Some(Permission::Admin);
637 }
638
639 if let Some(org) = self.get_organization_by_name(owner) {
641 if org.is_admin(user) {
642 return Some(Permission::Admin);
643 }
644 if org.is_member(user) {
645 best = Some(Permission::Read);
646 }
647 }
648 }
649
650 if let Some(collab) = self.get_collaborator(repo_key, user) {
652 if best.is_none() || collab.permission > best.unwrap() {
653 best = Some(collab.permission);
654 }
655 }
656
657 let user_teams = self.list_user_teams(user);
659 for team in user_teams {
660 if let Some(perm) = team.get_repo_permission(repo_key) {
661 if best.is_none() || perm > best.unwrap() {
662 best = Some(perm);
663 }
664 }
665 }
666
667 best
668 }
669
670 fn now() -> u64 {
671 std::time::SystemTime::now()
672 .duration_since(std::time::UNIX_EPOCH)
673 .unwrap_or_default()
674 .as_secs()
675 }
676}
677
678#[cfg(test)]
679mod tests {
680 use super::*;
681
682 #[test]
683 fn test_organization_crud() {
684 let store = AuthStore::new();
685
686 let org = store
688 .create_organization("acme".into(), "Acme Corp".into(), "owner".into())
689 .unwrap();
690 assert_eq!(org.name, "acme");
691
692 assert!(store
694 .create_organization("acme".into(), "Another".into(), "other".into())
695 .is_err());
696
697 let org2 = store.get_organization(org.id).unwrap();
699 assert_eq!(org2.name, "acme");
700
701 let org3 = store.get_organization_by_name("acme").unwrap();
703 assert_eq!(org3.id, org.id);
704
705 let org4 = store
707 .update_organization(org.id, Some("ACME Inc".into()), Some("Description".into()))
708 .unwrap();
709 assert_eq!(org4.display_name, "ACME Inc");
710 assert_eq!(org4.description, Some("Description".into()));
711
712 store.delete_organization(org.id).unwrap();
714 assert!(store.get_organization(org.id).is_none());
715 }
716
717 #[test]
718 fn test_team_crud() {
719 let store = AuthStore::new();
720 let org = store
721 .create_organization("acme".into(), "Acme Corp".into(), "owner".into())
722 .unwrap();
723
724 let team = store
726 .create_team(org.id, "backend".into(), Permission::Write, "owner".into())
727 .unwrap();
728 assert_eq!(team.name, "backend");
729
730 let teams = store.list_teams(org.id);
732 assert_eq!(teams.len(), 1);
733
734 store.add_team_member(team.id, "user1".into()).unwrap();
736 store.add_team_member(team.id, "user2".into()).unwrap();
737
738 let team = store.get_team(team.id).unwrap();
739 assert!(team.is_member("user1"));
740 assert!(team.is_member("user2"));
741
742 store.add_team_repo(team.id, "acme/api".into()).unwrap();
744 let team = store.get_team(team.id).unwrap();
745 assert!(team.has_repo("acme/api"));
746
747 store.delete_team(team.id).unwrap();
749 assert!(store.get_team(team.id).is_none());
750 }
751
752 #[test]
753 fn test_permission_resolution() {
754 let store = AuthStore::new();
755
756 assert!(store.check_permission("alice", "alice/repo", Permission::Admin));
758
759 assert!(!store.check_permission("bob", "alice/repo", Permission::Read));
761
762 store.set_collaborator(
764 "alice/repo".into(),
765 "bob".into(),
766 Permission::Write,
767 "alice".into(),
768 );
769 assert!(store.check_permission("bob", "alice/repo", Permission::Read));
770 assert!(store.check_permission("bob", "alice/repo", Permission::Write));
771 assert!(!store.check_permission("bob", "alice/repo", Permission::Admin));
772
773 let org = store
775 .create_organization("acme".into(), "Acme".into(), "owner".into())
776 .unwrap();
777 let team = store
778 .create_team(org.id, "devs".into(), Permission::Write, "owner".into())
779 .unwrap();
780 store.add_team_member(team.id, "charlie".into()).unwrap();
781 store.add_team_repo(team.id, "acme/api".into()).unwrap();
782
783 assert!(store.check_permission("charlie", "acme/api", Permission::Write));
785
786 store
788 .add_org_member(
789 org.id,
790 OrgMember::new("dave".into(), OrgRole::Admin, "owner".into()),
791 )
792 .unwrap();
793 assert!(store.check_permission("dave", "acme/api", Permission::Admin));
794 }
795
796 #[test]
797 fn test_branch_protection() {
798 let store = AuthStore::new();
799
800 let protection = store.set_branch_protection("alice/repo".into(), "main".into());
801 assert!(protection.matches("main"));
802
803 let found = store.find_branch_protection("alice/repo", "main");
805 assert!(found.is_some());
806
807 let not_found = store.find_branch_protection("alice/repo", "develop");
808 assert!(not_found.is_none());
809
810 let updated = store
812 .update_branch_protection("alice/repo", "main", |p| {
813 p.required_reviews = 2;
814 p.require_code_owner_review = true;
815 })
816 .unwrap();
817 assert_eq!(updated.required_reviews, 2);
818 assert!(updated.require_code_owner_review);
819
820 store
822 .remove_branch_protection("alice/repo", "main")
823 .unwrap();
824 assert!(store.find_branch_protection("alice/repo", "main").is_none());
825 }
826
827 #[test]
828 fn test_webhooks() {
829 use crate::webhook::WebhookEvent;
830 use std::collections::HashSet;
831
832 let store = AuthStore::new();
833
834 let mut events = HashSet::new();
835 events.insert(WebhookEvent::Push);
836 events.insert(WebhookEvent::PullRequest);
837
838 let webhook = store.create_webhook(
839 "alice/repo".into(),
840 "https://example.com/hook".into(),
841 events,
842 );
843 assert_eq!(webhook.id, 1);
844
845 let push_hooks = store.find_webhooks_for_event("alice/repo", WebhookEvent::Push);
847 assert_eq!(push_hooks.len(), 1);
848
849 let issue_hooks = store.find_webhooks_for_event("alice/repo", WebhookEvent::Issue);
850 assert_eq!(issue_hooks.len(), 0);
851
852 store.update_webhook(webhook.id, |w| w.disable()).unwrap();
854 let disabled_hooks = store.find_webhooks_for_event("alice/repo", WebhookEvent::Push);
855 assert_eq!(disabled_hooks.len(), 0);
856
857 store.delete_webhook(webhook.id).unwrap();
859 assert!(store.get_webhook(webhook.id).is_none());
860 }
861}