use crate::heddle::api::v1alpha2::{
ApprovalGroupRecord, ApprovalGroupView, ResourceRole, ReviewPolicyRecord, SuggestedPrincipal,
};
use crate::v2::people::validate_person;
use std::collections::BTreeSet;
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
pub enum ApprovalGroupError {
#[error("APPROVAL_ROLE_BELOW_ELIGIBILITY_FLOOR")]
RoleBelowEligibilityFloor,
#[error("approval-group administrator required")]
Administrator,
#[error("explicit member handle unavailable")]
HandleNotFound,
#[error("invalid ResourceRole")]
Role,
#[error("invalid resolved member metadata")]
Metadata,
#[error("approval group projection differs from live membership")]
Projection,
}
pub struct ApprovalPrincipal {
pub subject: String,
pub person: SuggestedPrincipal,
pub effective_role: ResourceRole,
pub is_agent: bool,
pub explicit_member: bool,
pub handle_visible: bool,
}
pub fn effective_resource_role(roles: &[ResourceRole]) -> ResourceRole {
roles
.iter()
.copied()
.max_by_key(|role| *role as i32)
.unwrap_or(ResourceRole::Unspecified)
}
fn validate_threshold(role: i32) -> Result<(), ApprovalGroupError> {
let role = ResourceRole::try_from(role).map_err(|_| ApprovalGroupError::Role)?;
if role == ResourceRole::Reader {
return Err(ApprovalGroupError::RoleBelowEligibilityFloor);
}
Ok(())
}
pub fn validate_approval_group(group: &ApprovalGroupRecord) -> Result<(), ApprovalGroupError> {
validate_threshold(group.member_role)?;
for handle in &group.explicit_member_handles {
validate_person(&SuggestedPrincipal {
handle: handle.clone(),
kind: 1,
..Default::default()
})
.map_err(|_| ApprovalGroupError::Metadata)?;
}
Ok(())
}
pub fn validate_review_policy(policy: &ReviewPolicyRecord) -> Result<(), ApprovalGroupError> {
validate_threshold(policy.minimum_role)
}
fn role_member(group: &ApprovalGroupRecord, principal: &ApprovalPrincipal) -> bool {
group.member_role != ResourceRole::Unspecified as i32
&& principal.effective_role as i32 >= group.member_role
}
fn is_member(group: &ApprovalGroupRecord, principal: &ApprovalPrincipal) -> bool {
!principal.is_agent
&& !principal.subject.is_empty()
&& principal.effective_role as i32 >= ResourceRole::Writer as i32
&& (principal.explicit_member || role_member(group, principal))
}
pub struct ApprovalGroupViewContext {
pub can_read_members: bool,
pub is_administrator: bool,
}
pub fn resolve_approval_group_members(
group: &ApprovalGroupRecord,
is_administrator: bool,
current: &[ApprovalPrincipal],
mut resolve_visible_human: impl FnMut(&str) -> Option<String>,
) -> Result<Vec<String>, ApprovalGroupError> {
if !is_administrator {
return Err(ApprovalGroupError::Administrator);
}
validate_approval_group(group)?;
group
.explicit_member_handles
.iter()
.map(|handle| {
resolve_visible_human(handle)
.filter(|subject| !subject.is_empty())
.ok_or(ApprovalGroupError::HandleNotFound)
})
.collect::<Result<BTreeSet<_>, _>>()
.map(|mut subjects| {
subjects.extend(
current
.iter()
.filter(|p| {
p.explicit_member
&& !p.is_agent
&& !p.subject.is_empty()
&& (!p.handle_visible || p.person.handle.is_empty())
})
.map(|p| p.subject.clone()),
);
subjects.into_iter().collect()
})
}
pub fn approval_group_view(
group: &ApprovalGroupRecord,
current: &[ApprovalPrincipal],
context: &ApprovalGroupViewContext,
) -> Result<ApprovalGroupView, ApprovalGroupError> {
validate_approval_group(group)?;
let members: Vec<_> = current.iter().filter(|p| is_member(group, p)).collect();
let count = |principals: Vec<&ApprovalPrincipal>| -> u32 {
principals
.iter()
.map(|p| &p.subject)
.collect::<BTreeSet<_>>()
.len() as u32
};
let mut view = ApprovalGroupView {
r#ref: group.r#ref.clone(),
version: group.version.clone(),
name: group.name.clone(),
description: group.description.clone(),
member_role: group.member_role,
resolved_member_count: count(members.clone()),
role_member_count: count(
members
.iter()
.copied()
.filter(|p| role_member(group, p))
.collect(),
),
explicit_member_count: count(
current
.iter()
.filter(|p| p.explicit_member && !p.is_agent && !p.subject.is_empty())
.collect(),
),
..Default::default()
};
if context.can_read_members {
for principal in &members {
if !principal.handle_visible || principal.person.handle.is_empty() {
continue;
}
validate_person(&principal.person).map_err(|_| ApprovalGroupError::Metadata)?;
view.resolved_members.push(principal.person.clone());
if role_member(group, principal) {
view.role_member_handles
.push(principal.person.handle.clone());
}
}
}
if context.is_administrator && context.can_read_members {
for principal in current
.iter()
.filter(|p| p.explicit_member && !p.is_agent && !p.subject.is_empty())
{
if !principal.handle_visible || principal.person.handle.is_empty() {
continue;
}
validate_person(&principal.person).map_err(|_| ApprovalGroupError::Metadata)?;
view.explicit_member_handles
.push(principal.person.handle.clone());
}
}
view.resolved_members
.sort_by(|a, b| a.handle.cmp(&b.handle));
view.resolved_members.dedup_by(|a, b| a.handle == b.handle);
view.role_member_handles.sort();
view.role_member_handles.dedup();
view.explicit_member_handles.sort();
view.explicit_member_handles.dedup();
Ok(view)
}
pub fn validate_approval_group_view(
view: &ApprovalGroupView,
group: &ApprovalGroupRecord,
current: &[ApprovalPrincipal],
context: &ApprovalGroupViewContext,
) -> Result<(), ApprovalGroupError> {
if view != &approval_group_view(group, current, context)? {
return Err(ApprovalGroupError::Projection);
}
Ok(())
}
pub fn group_approval_count(
group: &ApprovalGroupRecord,
policy: &ReviewPolicyRecord,
current: &[ApprovalPrincipal],
approvers: &[String],
) -> Result<usize, ApprovalGroupError> {
validate_approval_group(group)?;
ResourceRole::try_from(policy.minimum_role).map_err(|_| ApprovalGroupError::Role)?;
let minimum = policy.minimum_role.max(ResourceRole::Writer as i32);
let eligible: BTreeSet<_> = current
.iter()
.filter(|principal| {
is_member(group, principal) && principal.effective_role as i32 >= minimum
})
.map(|principal| &principal.subject)
.collect();
Ok(approvers
.iter()
.filter(|subject| eligible.contains(subject))
.collect::<BTreeSet<_>>()
.len())
}