use std::collections::BTreeMap;
use prost_types::Timestamp;
use crate::heddle::api::common::{CallFailure, CallFailureCode, ErrorDetail, ErrorReason};
use crate::heddle::api::v1alpha2::{
ActionAvailability, Capability, EndpointRef, EntityRef, ExpectedVersion, GrantRecord,
RecordRef, Requirement, RequirementKind, ResourceRole, entity_ref,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MemberKind {
Human,
Agent,
}
#[derive(Clone, Debug)]
pub struct MembershipCandidate {
pub grant: RecordRef,
pub subject: String,
pub role: ResourceRole,
pub kind: MemberKind,
pub personal_owner: bool,
pub expires_at: Option<Timestamp>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MembershipField {
Grant,
Role,
ExpiresAt,
}
impl MembershipField {
pub const fn as_str(self) -> &'static str {
match self {
Self::Grant => "grant",
Self::Role => "grant.role",
Self::ExpiresAt => "grant.expires_at",
}
}
}
#[derive(Clone, Debug)]
pub struct MembershipChange {
pub grant: RecordRef,
pub role: ResourceRole,
pub expires_at: Option<Timestamp>,
pub field: MembershipField,
}
pub struct MembershipFloorContext<'a> {
pub now: &'a Timestamp,
pub operation_authorized: bool,
pub can_manage_grants: bool,
pub existing_refusal: &'a CallFailure,
}
fn live(role: ResourceRole, expiry: Option<&Timestamp>, now: &Timestamp) -> bool {
role != ResourceRole::Unspecified
&& expiry.is_none_or(|t| (t.seconds, t.nanos) > (now.seconds, now.nanos))
}
pub fn membership_floor(
candidates: &[MembershipCandidate],
change: &MembershipChange,
context: &MembershipFloorContext<'_>,
) -> Option<CallFailure> {
membership_floor_batch(
candidates,
std::slice::from_ref(change),
change.field,
context,
)
}
pub fn membership_floor_batch(
candidates: &[MembershipCandidate],
changes: &[MembershipChange],
field: MembershipField,
context: &MembershipFloorContext<'_>,
) -> Option<CallFailure> {
if !context.operation_authorized {
return Some(context.existing_refusal.clone());
}
let mut members = BTreeMap::new();
for candidate in candidates {
if candidate.kind != MemberKind::Human {
continue;
}
let (role, expiry) =
if let Some(change) = changes.iter().find(|c| c.grant == candidate.grant) {
(change.role, change.expires_at.as_ref())
} else {
(candidate.role, candidate.expires_at.as_ref())
};
if live(role, expiry, context.now) {
let current = members
.entry(&candidate.subject)
.or_insert(ResourceRole::Unspecified);
if role as i32 > *current as i32 {
*current = role;
}
}
}
let owner_lost = candidates.iter().any(|p| {
p.kind == MemberKind::Human && p.personal_owner && !members.contains_key(&p.subject)
});
let reason = if owner_lost {
ErrorReason::PersonalSpoolOwnerAccessRequired
} else if members.is_empty() {
ErrorReason::SpoolLastMember
} else if !members.is_empty()
&& !members
.values()
.any(|role| *role == ResourceRole::Administrator)
{
ErrorReason::SpoolLastAdministrator
} else {
return None;
};
if !context.can_manage_grants {
return Some(context.existing_refusal.clone());
}
Some(CallFailure {
code: CallFailureCode::FailedPrecondition as i32,
message: "spool membership required".into(),
error: Some(ErrorDetail {
reason: reason as i32,
field: field.as_str().into(),
..Default::default()
}),
})
}
pub struct MembershipAdviceContext<'a> {
pub floor: MembershipFloorContext<'a>,
pub endpoint: &'a EndpointRef,
pub implemented: bool,
}
pub fn membership_floor_actions(
visible_grants: &[GrantRecord],
candidates: &[MembershipCandidate],
context: &MembershipAdviceContext<'_>,
) -> Vec<ActionAvailability> {
let mut actions = Vec::new();
for grant in visible_grants {
let Some(reference) = &grant.r#ref else {
continue;
};
for (method, capability, role, field) in [
(
"RevokeGrant",
Capability::RevokeGrant,
ResourceRole::Unspecified,
MembershipField::Grant,
),
(
"PutGrant",
Capability::PutGrant,
ResourceRole::Writer,
MembershipField::Role,
),
] {
let target = EntityRef {
entity: Some(entity_ref::Entity::Grant(reference.clone())),
};
let change = MembershipChange {
grant: reference.clone(),
role,
expires_at: grant.expires_at,
field,
};
let requirements = match membership_floor(candidates, &change, &context.floor) {
None => Vec::new(),
Some(failure) => vec![Requirement {
kind: if context.floor.operation_authorized {
RequirementKind::Policy
} else {
RequirementKind::Capability
} as i32,
subject: Some(target.clone()),
error: if context.floor.can_manage_grants && context.floor.operation_authorized
{
failure.error
} else {
None
},
..Default::default()
}],
};
actions.push(ActionAvailability {
method: format!("/heddle.api.v1alpha2.SpoolService/{method}"),
endpoint: Some(context.endpoint.clone()),
target: Some(target.clone()),
implemented: context.implemented,
authorized: context.floor.operation_authorized,
requirements,
capability: capability as i32,
observed_versions: vec![ExpectedVersion {
resource: Some(target),
version: grant.version.clone(),
}],
});
}
}
actions
}