Skip to main content

ephemeris_forge_github/plan/
access.rs

1use ephemeris_forge::{
2    Access, AccessDecision, AccessOperation, CollaboratorDecision, Limitation, PermissionScope,
3    Report, TeamName,
4};
5
6use crate::api::model::OwnerKind;
7use crate::operation::access::Operation;
8use crate::{Live, Permission};
9
10pub(super) fn plan(
11    config: Option<&Access<Permission>>,
12    live: &Live,
13    report: &mut Report,
14) -> Vec<Operation> {
15    let Some(access) = config else {
16        return Vec::new();
17    };
18    let personal = live.owner == Some(OwnerKind::User);
19    let grants = live.access.grants(PermissionScope::Repository);
20    let mut operations = Vec::new();
21    for decision in access.plan(&grants) {
22        if personal && let Some((team, role)) = custom_team(&decision) {
23            report
24                .limitations
25                .push(Limitation::TeamRoleNeedsOrganization {
26                    team: team.clone(),
27                    role: role.as_str().to_owned(),
28                });
29            continue;
30        }
31        decision.record(report, "GitHub");
32        operations.extend(team_operation(decision));
33    }
34    let mut present = live.access.collaborators.clone();
35    present.extend(
36        live.invitations
37            .iter()
38            .map(|(user, invitation)| (user.clone(), invitation.permission.clone())),
39    );
40    for decision in access.plan_collaborators(&present, live.access.caller.as_ref()) {
41        if personal && let Some((user, role)) = custom_collaborator(&decision) {
42            report
43                .limitations
44                .push(Limitation::CollaboratorRoleNeedsOrganization {
45                    user: user.clone(),
46                    role: role.as_str().to_owned(),
47                });
48            continue;
49        }
50        decision.record(report);
51        operations.extend(collaborator_operation(decision, live));
52    }
53    operations
54}
55
56fn custom_team(decision: &AccessDecision<Permission>) -> Option<(&TeamName, &Permission)> {
57    match decision {
58        AccessDecision::Add { team, permission }
59        | AccessDecision::PermissionMismatch {
60            team,
61            expected: permission,
62            actual: _,
63        }
64        | AccessDecision::OrganizationPermission {
65            team,
66            expected: permission,
67            actual: _,
68        } => permission.custom().map(|_role| (team, permission)),
69        AccessDecision::Remove { team: _ }
70        | AccessDecision::NotInOrganization { team: _ }
71        | AccessDecision::SelfRemoval { team: _ }
72        | AccessDecision::Matches { team: _ }
73        | AccessDecision::Inherited { team: _ } => None,
74    }
75}
76
77fn custom_collaborator(
78    decision: &CollaboratorDecision<Permission>,
79) -> Option<(&ephemeris_forge::UserName, &Permission)> {
80    match decision {
81        CollaboratorDecision::Add { user, permission }
82        | CollaboratorDecision::Change {
83            user,
84            permission,
85            actual: _,
86        } => permission.custom().map(|_role| (user, permission)),
87        CollaboratorDecision::Remove { user: _ }
88        | CollaboratorDecision::SelfRemoval { user: _ }
89        | CollaboratorDecision::Matches { user: _ } => None,
90    }
91}
92
93fn team_operation(decision: AccessDecision<Permission>) -> Option<Operation> {
94    match decision {
95        AccessDecision::Add { team, permission } => {
96            Some(Operation::Grant(AccessOperation::AddTeam {
97                team,
98                permission,
99            }))
100        }
101        AccessDecision::Remove { team } => {
102            Some(Operation::Grant(AccessOperation::RemoveTeam { team }))
103        }
104        AccessDecision::PermissionMismatch {
105            team,
106            expected,
107            actual: _,
108        } => Some(Operation::Grant(AccessOperation::AddTeam {
109            team,
110            permission: expected,
111        })),
112        AccessDecision::NotInOrganization { team: _ }
113        | AccessDecision::OrganizationPermission {
114            team: _,
115            expected: _,
116            actual: _,
117        }
118        | AccessDecision::Matches { team: _ }
119        | AccessDecision::SelfRemoval { team: _ }
120        | AccessDecision::Inherited { team: _ } => None,
121    }
122}
123
124fn collaborator_operation(
125    decision: CollaboratorDecision<Permission>,
126    live: &Live,
127) -> Option<Operation> {
128    match decision {
129        CollaboratorDecision::Add { user, permission } => {
130            Some(Operation::Grant(AccessOperation::AddCollaborator {
131                user,
132                permission,
133            }))
134        }
135        CollaboratorDecision::Change {
136            user,
137            permission,
138            actual: _,
139        } => Some(match live.invitations.get(&user) {
140            Some(invitation) if permission.invitation_name().is_some() => {
141                Operation::SetInvitationPermission {
142                    id: invitation.id,
143                    user,
144                    permission,
145                }
146            }
147            Some(_) | None => {
148                Operation::Grant(AccessOperation::AddCollaborator { user, permission })
149            }
150        }),
151        CollaboratorDecision::Remove { user } => Some(match live.invitations.get(&user) {
152            Some(invitation) => Operation::CancelInvitation {
153                id: invitation.id,
154                user,
155            },
156            None => Operation::Grant(AccessOperation::RemoveCollaborator { user }),
157        }),
158        CollaboratorDecision::Matches { user: _ }
159        | CollaboratorDecision::SelfRemoval { user: _ } => None,
160    }
161}