use std::collections::HashSet;
use auths_core::storage::keychain::IdentityDID;
use auths_verifier::core::{Attestation, Capability, ResourceId, Role};
use auths_verifier::types::CanonicalDid;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone)]
pub struct MemberFilter {
pub include_statuses: HashSet<MemberStatusKind>,
pub roles_any: Option<HashSet<Role>>,
pub capabilities_any: Option<HashSet<Capability>>,
pub capabilities_all: Option<HashSet<Capability>>,
pub now: Option<DateTime<Utc>>,
}
impl Default for MemberFilter {
fn default() -> Self {
let mut include_statuses = HashSet::new();
include_statuses.insert(MemberStatusKind::Active);
Self {
include_statuses,
roles_any: None,
capabilities_any: None,
capabilities_all: None,
now: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MemberStatusKind {
Active,
Revoked,
Expired,
Invalid,
}
impl MemberStatusKind {
pub fn rank(self) -> u8 {
match self {
MemberStatusKind::Active => 0,
MemberStatusKind::Revoked => 1,
MemberStatusKind::Expired => 2,
MemberStatusKind::Invalid => 3,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MemberStatus {
Active,
Revoked,
Expired { expires_at: DateTime<Utc> },
Invalid { reason: MemberInvalidReason },
}
impl MemberStatus {
pub fn kind(&self) -> MemberStatusKind {
match self {
MemberStatus::Active => MemberStatusKind::Active,
MemberStatus::Revoked => MemberStatusKind::Revoked,
MemberStatus::Expired { .. } => MemberStatusKind::Expired,
MemberStatus::Invalid { .. } => MemberStatusKind::Invalid,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MemberInvalidReason {
JsonParseError(String),
SubjectMismatch {
filename_did: CanonicalDid,
attestation_subject: CanonicalDid,
},
IssuerMismatch {
expected_issuer: IdentityDID,
actual_issuer: IdentityDID,
},
Other(String),
}
pub struct OrgMemberEntry {
pub org: IdentityDID,
pub did: CanonicalDid,
pub filename: String,
pub attestation: Result<Attestation, MemberInvalidReason>,
}
#[derive(Debug, Clone)]
pub struct MemberView {
pub did: CanonicalDid,
pub status: MemberStatus,
pub role: Option<Role>,
pub capabilities: Vec<Capability>,
pub issuer: IdentityDID,
pub rid: ResourceId,
pub revoked_at: Option<DateTime<Utc>>,
pub expires_at: Option<DateTime<Utc>>,
pub timestamp: Option<DateTime<Utc>>,
pub source_filename: String,
}
pub fn compute_status(att: &Attestation, now: DateTime<Utc>) -> MemberStatus {
if att.is_revoked() {
MemberStatus::Revoked
} else if let Some(expires_at) = att.expires_at {
if expires_at <= now {
MemberStatus::Expired { expires_at }
} else {
MemberStatus::Active
}
} else {
MemberStatus::Active
}
}
pub fn expected_org_issuer(org: &str) -> String {
format!("did:keri:{}", org)
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use auths_verifier::AttestationBuilder;
#[test]
fn member_filter_defaults_to_active_only() {
let filter = MemberFilter::default();
assert!(filter.include_statuses.contains(&MemberStatusKind::Active));
assert_eq!(filter.include_statuses.len(), 1);
}
#[test]
fn member_status_kind_rank_is_correct() {
assert!(MemberStatusKind::Active.rank() < MemberStatusKind::Revoked.rank());
assert!(MemberStatusKind::Revoked.rank() < MemberStatusKind::Expired.rank());
assert!(MemberStatusKind::Expired.rank() < MemberStatusKind::Invalid.rank());
}
#[test]
fn member_status_kind_returns_correct_kind() {
assert_eq!(MemberStatus::Active.kind(), MemberStatusKind::Active);
assert_eq!(MemberStatus::Revoked.kind(), MemberStatusKind::Revoked);
assert_eq!(
MemberStatus::Expired {
expires_at: Utc::now()
}
.kind(),
MemberStatusKind::Expired
);
assert_eq!(
MemberStatus::Invalid {
reason: MemberInvalidReason::Other("test".into())
}
.kind(),
MemberStatusKind::Invalid
);
}
#[test]
fn compute_status_active() {
let att = AttestationBuilder::default()
.rid("test")
.issuer("did:keri:Eissuer")
.subject("did:key:zSubject")
.build();
let now = Utc::now();
assert_eq!(compute_status(&att, now), MemberStatus::Active);
}
#[test]
fn compute_status_revoked() {
let att = AttestationBuilder::default()
.rid("test")
.issuer("did:keri:Eissuer")
.subject("did:key:zSubject")
.revoked_at(Some(Utc::now()))
.build();
let now = Utc::now();
assert_eq!(compute_status(&att, now), MemberStatus::Revoked);
}
#[test]
fn compute_status_expired() {
let past = Utc::now() - chrono::Duration::hours(1);
let att = AttestationBuilder::default()
.rid("test")
.issuer("did:keri:Eissuer")
.subject("did:key:zSubject")
.expires_at(Some(past))
.build();
let now = Utc::now();
assert!(matches!(
compute_status(&att, now),
MemberStatus::Expired { .. }
));
}
#[test]
fn compute_status_not_expired_yet() {
let future = Utc::now() + chrono::Duration::hours(1);
let att = AttestationBuilder::default()
.rid("test")
.issuer("did:keri:Eissuer")
.subject("did:key:zSubject")
.expires_at(Some(future))
.build();
let now = Utc::now();
assert_eq!(compute_status(&att, now), MemberStatus::Active);
}
#[test]
fn compute_status_expired_at_boundary() {
let now = Utc::now();
let att = AttestationBuilder::default()
.rid("test")
.issuer("did:keri:Eissuer")
.subject("did:key:zSubject")
.expires_at(Some(now))
.build();
assert!(matches!(
compute_status(&att, now),
MemberStatus::Expired { .. }
));
}
#[test]
fn expected_org_issuer_formats_correctly() {
assert_eq!(
expected_org_issuer("EOrg1234567890"),
"did:keri:EOrg1234567890"
);
}
}