use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::capability::{AdminAuthority, CapRef, Capability, ResourceQualifier};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct GroupGrant {
pub capability: Capability,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resource: Option<ResourceQualifier>,
pub group_id: String,
pub binding_id: String,
pub delegated_by: String,
pub granted_at: DateTime<Utc>,
}
impl GroupGrant {
pub fn cap_ref(&self) -> CapRef {
CapRef::new(self.capability, self.resource.clone())
}
pub fn in_effect(&self, admin: &AdminAuthority) -> bool {
admin.act.is_all() && admin.ceiling_admits(&self.cap_ref())
}
pub fn same_row(&self, other: &GroupGrant) -> bool {
self.capability == other.capability
&& self.resource == other.resource
&& self.group_id == other.group_id
&& self.binding_id == other.binding_id
}
}
pub fn effective(admin: &AdminAuthority, grants: &[GroupGrant]) -> Vec<CapRef> {
grants
.iter()
.filter(|g| g.in_effect(admin))
.map(GroupGrant::cap_ref)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::acl::{AdminRole, CapabilityGrant, CapabilityScope, VtcAclEntry, VtcRole};
fn row(cap: Capability, group: &str) -> GroupGrant {
GroupGrant {
capability: cap,
resource: None,
group_id: group.into(),
binding_id: format!("gb_{group}"),
delegated_by: "did:key:zBinder".into(),
granted_at: Utc::now(),
}
}
fn narrowed_moderator() -> VtcAclEntry {
let mut admin = AdminAuthority::for_role(AdminRole::Moderator);
admin.capabilities = CapabilityScope::Listed {
grants: vec![CapabilityGrant::from(CapRef::all(Capability::JoinDecide))],
};
VtcAclEntry::new("did:key:zM", VtcRole::Member, admin, "did:key:zI")
}
#[test]
fn vti_dir_102_a_group_grant_confers_only_inside_the_ceiling() {
let mut e = narrowed_moderator();
assert!(!e.can(Capability::InvitationsManage, None));
e.group_grants = vec![
row(Capability::InvitationsManage, "grp_a"),
row(Capability::RolesAssign, "grp_a"),
];
assert!(e.can(Capability::InvitationsManage, None));
assert!(!e.can(Capability::RolesAssign, None));
assert!(e.holds(&CapRef::all(Capability::InvitationsManage)));
assert!(
e.capability_list()
.contains(&"vtc.invitations.manage".to_string())
);
assert!(
!e.capability_list()
.contains(&"vtc.roles.assign".to_string())
);
}
#[test]
fn vti_dir_105_a_group_never_supplies_the_ceiling() {
let mut plain = VtcAclEntry::new(
"did:key:zP",
VtcRole::Member,
AdminAuthority::none(),
"did:key:zI",
);
plain.group_grants = vec![row(Capability::InvitationsManage, "grp_a")];
assert!(!plain.can(Capability::InvitationsManage, None));
let mut idle = narrowed_moderator();
idle.admin.act = crate::acl::VtcActScope::None;
idle.group_grants = vec![row(Capability::InvitationsManage, "grp_a")];
assert!(!idle.can(Capability::InvitationsManage, None));
}
#[test]
fn a_group_grant_confers_nothing_to_approve() {
let mut e = narrowed_moderator();
e.admin.approve = crate::acl::VtcActScope::None;
e.group_grants = vec![row(Capability::MembersManage, "grp_a")];
assert!(e.can(Capability::MembersManage, None));
assert!(!e.can_approve(&CapRef::all(Capability::MembersManage)));
}
#[test]
fn a_conferring_group_grant_is_counted_as_conferred() {
let mut e = VtcAclEntry::new(
"did:key:zC",
VtcRole::Member,
AdminAuthority::community_admin(),
"did:key:zI",
);
e.admin.capabilities = CapabilityScope::Listed {
grants: vec![CapabilityGrant::from(CapRef::all(Capability::AuditRead))],
};
let before = e.clone();
e.group_grants = vec![row(Capability::RolesAssign, "grp_a")];
let now = vti_common::auth::session::now_epoch();
assert_eq!(
crate::acl::admin_consent::newly_conferred(Some(&before), &e, now),
vec![CapRef::all(Capability::RolesAssign)]
);
assert_eq!(
crate::acl::admin_consent::lost_conferring(&e, Some(&before), now),
vec![CapRef::all(Capability::RolesAssign)]
);
}
#[test]
fn a_row_round_trips() {
let mut g = row(Capability::RoomsManage, "grp_a");
g.resource = Some(ResourceQualifier::Group("grp_b".into()));
let v = serde_json::to_value(&g).unwrap();
assert_eq!(v["groupId"], "grp_a");
assert_eq!(serde_json::from_value::<GroupGrant>(v).unwrap(), g);
}
}