use crate::identity::IdentityId;
use crate::index::{ReceiptIndex, TrustIndex};
use crate::receipt::{ActionReceipt, ActionType, ReceiptId};
use crate::trust::TrustGrant;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SortOrder {
#[default]
NewestFirst,
OldestFirst,
}
#[derive(Debug, Clone, Default)]
pub struct ReceiptQuery {
pub actor: Option<IdentityId>,
pub action_type: Option<ActionType>,
pub time_range: Option<(u64, u64)>,
pub chain_root: Option<ReceiptId>,
pub limit: Option<usize>,
pub sort: SortOrder,
}
#[derive(Debug, Clone, Default)]
pub struct TrustQuery {
pub grantor: Option<IdentityId>,
pub grantee: Option<IdentityId>,
pub capability_prefix: Option<String>,
pub valid_only: bool,
pub limit: Option<usize>,
}
pub fn query_receipts<'a>(index: &'a ReceiptIndex, query: &ReceiptQuery) -> Vec<&'a ActionReceipt> {
let mut candidates: Vec<&ActionReceipt> =
match (&query.actor, &query.action_type, &query.time_range) {
(Some(actor), _, _) => index.by_actor(actor),
(None, Some(atype), _) => index.by_type(atype),
(None, None, Some((from, to))) => index.by_time_range(*from, *to),
(None, None, None) => {
index.by_time_range(0, u64::MAX)
}
};
if let Some(actor) = &query.actor {
candidates.retain(|r| &r.actor == actor);
}
if let Some(atype) = &query.action_type {
candidates.retain(|r| r.action_type.as_tag() == atype.as_tag());
}
if let Some((from, to)) = &query.time_range {
candidates.retain(|r| r.timestamp >= *from && r.timestamp <= *to);
}
if let Some(root) = &query.chain_root {
candidates.retain(|r| r.previous_receipt.as_ref() == Some(root));
}
match query.sort {
SortOrder::NewestFirst => {
candidates.sort_unstable_by(|a, b| b.timestamp.cmp(&a.timestamp));
}
SortOrder::OldestFirst => {
candidates.sort_unstable_by(|a, b| a.timestamp.cmp(&b.timestamp));
}
}
if let Some(limit) = query.limit {
candidates.truncate(limit);
}
candidates
}
pub fn query_trust<'a>(index: &'a TrustIndex, query: &TrustQuery) -> Vec<&'a TrustGrant> {
let mut candidates: Vec<&TrustGrant> = match (&query.grantor, &query.grantee) {
(Some(grantor), _) => index.by_grantor(grantor),
(None, Some(grantee)) => index.by_grantee(grantee),
(None, None) => {
collect_all_grants(index)
}
};
if let Some(grantor) = &query.grantor {
candidates.retain(|g| &g.grantor == grantor);
}
if let Some(grantee) = &query.grantee {
candidates.retain(|g| &g.grantee == grantee);
}
if let Some(prefix) = &query.capability_prefix {
candidates.retain(|g| {
g.capabilities
.iter()
.any(|c| c.uri.starts_with(prefix.as_str()))
});
}
if query.valid_only {
let now = crate::time::now_micros();
candidates.retain(|g| g.constraints.is_time_valid(now) && !index.is_revoked(&g.id));
}
if let Some(limit) = query.limit {
candidates.truncate(limit);
}
candidates
}
fn collect_all_grants(index: &TrustIndex) -> Vec<&TrustGrant> {
index.iter_all_grants()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::IdentityAnchor;
use crate::index::{ReceiptIndex, TrustIndex};
use crate::receipt::action::{ActionContent, ActionType};
use crate::receipt::receipt::ReceiptBuilder;
use crate::trust::capability::Capability;
use crate::trust::constraint::TrustConstraints;
use crate::trust::grant::TrustGrantBuilder;
use crate::trust::revocation::{Revocation, RevocationReason};
fn make_receipt(anchor: &IdentityAnchor, atype: ActionType, desc: &str) -> ActionReceipt {
ReceiptBuilder::new(anchor.id(), atype, ActionContent::new(desc))
.sign(anchor.signing_key())
.expect("sign receipt")
}
fn grantee_key(a: &IdentityAnchor) -> String {
base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
a.verifying_key_bytes(),
)
}
fn make_grant(grantor: &IdentityAnchor, grantee: &IdentityAnchor, cap: &str) -> TrustGrant {
TrustGrantBuilder::new(grantor.id(), grantee.id(), grantee_key(grantee))
.capability(Capability::new(cap))
.sign(grantor.signing_key())
.expect("sign grant")
}
fn make_grant_with_constraints(
grantor: &IdentityAnchor,
grantee: &IdentityAnchor,
cap: &str,
constraints: TrustConstraints,
) -> TrustGrant {
TrustGrantBuilder::new(grantor.id(), grantee.id(), grantee_key(grantee))
.capability(Capability::new(cap))
.constraints(constraints)
.sign(grantor.signing_key())
.expect("sign grant")
}
#[test]
fn test_receipt_query_by_actor() {
let a1 = IdentityAnchor::new(Some("actor-1".into()));
let a2 = IdentityAnchor::new(Some("actor-2".into()));
let a3 = IdentityAnchor::new(Some("actor-3".into()));
let mut idx = ReceiptIndex::new();
idx.insert(make_receipt(&a1, ActionType::Decision, "a1 decision 1"));
idx.insert(make_receipt(&a1, ActionType::Observation, "a1 observation"));
idx.insert(make_receipt(&a2, ActionType::Decision, "a2 decision"));
idx.insert(make_receipt(&a2, ActionType::Mutation, "a2 mutation"));
idx.insert(make_receipt(&a3, ActionType::Decision, "a3 decision"));
let q = ReceiptQuery {
actor: Some(a1.id()),
..Default::default()
};
let results = query_receipts(&idx, &q);
assert_eq!(results.len(), 2, "expected exactly a1's 2 receipts");
assert!(
results.iter().all(|r| r.actor == a1.id()),
"all returned receipts must belong to actor-1"
);
}
#[test]
fn test_receipt_query_by_type() {
let anchor = IdentityAnchor::new(None);
let mut idx = ReceiptIndex::new();
idx.insert(make_receipt(&anchor, ActionType::Decision, "decision A"));
idx.insert(make_receipt(&anchor, ActionType::Decision, "decision B"));
idx.insert(make_receipt(
&anchor,
ActionType::Observation,
"observation",
));
idx.insert(make_receipt(&anchor, ActionType::Mutation, "mutation"));
idx.insert(make_receipt(
&anchor,
ActionType::Custom("audit".into()),
"audit event",
));
let q = ReceiptQuery {
action_type: Some(ActionType::Decision),
..Default::default()
};
let results = query_receipts(&idx, &q);
assert_eq!(results.len(), 2, "expected 2 Decision receipts");
assert!(
results
.iter()
.all(|r| r.action_type == ActionType::Decision),
"all returned receipts must be Decision type"
);
}
#[test]
fn test_receipt_query_by_time_range() {
let anchor = IdentityAnchor::new(None);
let mut idx = ReceiptIndex::new();
let r1 = make_receipt(&anchor, ActionType::Decision, "early");
std::thread::sleep(std::time::Duration::from_millis(2));
let r2 = make_receipt(&anchor, ActionType::Decision, "middle");
std::thread::sleep(std::time::Duration::from_millis(2));
let r3 = make_receipt(&anchor, ActionType::Decision, "late");
let t1 = r1.timestamp;
let t2 = r2.timestamp;
let t3 = r3.timestamp;
idx.insert(r1);
idx.insert(r2);
idx.insert(r3);
let q_first = ReceiptQuery {
time_range: Some((0, t1)),
..Default::default()
};
let first = query_receipts(&idx, &q_first);
assert_eq!(first.len(), 1, "expected only the earliest receipt");
assert!(first[0].timestamp <= t1);
let q_middle = ReceiptQuery {
time_range: Some((t2, t2)),
..Default::default()
};
let middle = query_receipts(&idx, &q_middle);
assert_eq!(middle.len(), 1);
assert_eq!(middle[0].timestamp, t2);
let q_all = ReceiptQuery {
time_range: Some((t1, t3)),
..Default::default()
};
let all = query_receipts(&idx, &q_all);
assert_eq!(all.len(), 3);
let q_none = ReceiptQuery {
time_range: Some((0, t1 - 1)),
..Default::default()
};
let none = query_receipts(&idx, &q_none);
assert!(none.is_empty());
}
#[test]
fn test_receipt_query_sort_order() {
let anchor = IdentityAnchor::new(None);
let mut idx = ReceiptIndex::new();
let r1 = make_receipt(&anchor, ActionType::Decision, "oldest");
std::thread::sleep(std::time::Duration::from_millis(2));
let r2 = make_receipt(&anchor, ActionType::Decision, "middle");
std::thread::sleep(std::time::Duration::from_millis(2));
let r3 = make_receipt(&anchor, ActionType::Decision, "newest");
idx.insert(r1);
idx.insert(r2);
idx.insert(r3);
let q_newest = ReceiptQuery {
sort: SortOrder::NewestFirst,
..Default::default()
};
let newest_first = query_receipts(&idx, &q_newest);
assert_eq!(newest_first.len(), 3);
assert!(
newest_first[0].timestamp >= newest_first[1].timestamp,
"first result should be newest"
);
assert!(
newest_first[1].timestamp >= newest_first[2].timestamp,
"results should be in descending order"
);
let q_oldest = ReceiptQuery {
sort: SortOrder::OldestFirst,
..Default::default()
};
let oldest_first = query_receipts(&idx, &q_oldest);
assert_eq!(oldest_first.len(), 3);
assert!(
oldest_first[0].timestamp <= oldest_first[1].timestamp,
"first result should be oldest"
);
assert!(
oldest_first[1].timestamp <= oldest_first[2].timestamp,
"results should be in ascending order"
);
assert_eq!(
newest_first[0].id, oldest_first[2].id,
"newest-first[0] should equal oldest-first[2]"
);
assert_eq!(
newest_first[2].id, oldest_first[0].id,
"newest-first[2] should equal oldest-first[0]"
);
}
#[test]
fn test_receipt_query_limit() {
let anchor = IdentityAnchor::new(None);
let mut idx = ReceiptIndex::new();
for i in 0..10 {
idx.insert(make_receipt(
&anchor,
ActionType::Observation,
&format!("obs {i}"),
));
}
let q = ReceiptQuery {
limit: Some(3),
sort: SortOrder::NewestFirst,
..Default::default()
};
let results = query_receipts(&idx, &q);
assert_eq!(results.len(), 3);
}
#[test]
fn test_trust_query_by_grantor() {
let g1 = IdentityAnchor::new(Some("grantor-1".into()));
let g2 = IdentityAnchor::new(Some("grantor-2".into()));
let tee = IdentityAnchor::new(Some("grantee".into()));
let mut idx = TrustIndex::new();
idx.insert_grant(make_grant(&g1, &tee, "read:*"));
idx.insert_grant(make_grant(&g1, &tee, "write:calendar"));
idx.insert_grant(make_grant(&g2, &tee, "read:calendar"));
let q = TrustQuery {
grantor: Some(g1.id()),
..Default::default()
};
let results = query_trust(&idx, &q);
assert_eq!(results.len(), 2, "expected grantor-1's 2 grants");
assert!(
results.iter().all(|g| g.grantor == g1.id()),
"all results must be from grantor-1"
);
}
#[test]
fn test_trust_query_by_capability() {
let grantor = IdentityAnchor::new(None);
let tee = IdentityAnchor::new(None);
let mut idx = TrustIndex::new();
idx.insert_grant(make_grant(&grantor, &tee, "read:calendar"));
idx.insert_grant(make_grant(&grantor, &tee, "read:email"));
idx.insert_grant(make_grant(&grantor, &tee, "write:calendar"));
idx.insert_grant(make_grant(&grantor, &tee, "execute:deploy:production"));
let q = TrustQuery {
capability_prefix: Some("read:".to_string()),
..Default::default()
};
let results = query_trust(&idx, &q);
assert_eq!(results.len(), 2, "expected 2 read:* grants");
assert!(
results
.iter()
.all(|g| { g.capabilities.iter().any(|c| c.uri.starts_with("read:")) }),
"all returned grants must contain a read: capability"
);
let q_write = TrustQuery {
capability_prefix: Some("write:".to_string()),
..Default::default()
};
let write_results = query_trust(&idx, &q_write);
assert_eq!(write_results.len(), 1);
assert!(write_results[0]
.capabilities
.iter()
.any(|c| c.uri.starts_with("write:")));
let q_exec = TrustQuery {
capability_prefix: Some("execute:".to_string()),
..Default::default()
};
let exec_results = query_trust(&idx, &q_exec);
assert_eq!(exec_results.len(), 1);
let q_none = TrustQuery {
capability_prefix: Some("admin:".to_string()),
..Default::default()
};
assert!(query_trust(&idx, &q_none).is_empty());
}
#[test]
fn test_trust_query_valid_only() {
let grantor = IdentityAnchor::new(None);
let tee = IdentityAnchor::new(None);
let now = crate::time::now_micros();
let valid_grant = make_grant(&grantor, &tee, "read:calendar");
let expired_grant = make_grant_with_constraints(
&grantor,
&tee,
"read:email",
TrustConstraints::time_bounded(now - 2_000_000, now - 1_000_000),
);
let not_yet_valid_grant = make_grant_with_constraints(
&grantor,
&tee,
"write:calendar",
TrustConstraints::time_bounded(now + 1_000_000, now + 2_000_000),
);
let revoked_grant = make_grant(&grantor, &tee, "execute:deploy");
let revoked_id = revoked_grant.id.clone();
let mut idx = TrustIndex::new();
idx.insert_grant(valid_grant.clone());
idx.insert_grant(expired_grant);
idx.insert_grant(not_yet_valid_grant);
idx.insert_grant(revoked_grant);
let rev = Revocation::create(
revoked_id.clone(),
grantor.id(),
RevocationReason::ManualRevocation,
grantor.signing_key(),
);
idx.insert_revocation(rev);
let q = TrustQuery {
valid_only: true,
..Default::default()
};
let results = query_trust(&idx, &q);
assert_eq!(results.len(), 1, "expected exactly 1 valid grant");
assert_eq!(
results[0].id, valid_grant.id,
"the valid grant must be the one returned"
);
let q_all = TrustQuery {
valid_only: false,
..Default::default()
};
let all = query_trust(&idx, &q_all);
assert_eq!(all.len(), 4);
}
#[test]
fn test_trust_query_combined_filters() {
let g1 = IdentityAnchor::new(None);
let g2 = IdentityAnchor::new(None);
let tee = IdentityAnchor::new(None);
let mut idx = TrustIndex::new();
idx.insert_grant(make_grant(&g1, &tee, "read:calendar"));
idx.insert_grant(make_grant(&g1, &tee, "write:calendar"));
idx.insert_grant(make_grant(&g2, &tee, "read:email"));
let q = TrustQuery {
grantor: Some(g1.id()),
capability_prefix: Some("read:".to_string()),
..Default::default()
};
let results = query_trust(&idx, &q);
assert_eq!(results.len(), 1);
assert_eq!(results[0].grantor, g1.id());
assert!(results[0]
.capabilities
.iter()
.any(|c| c.uri.starts_with("read:")));
}
#[test]
fn test_trust_query_limit() {
let grantor = IdentityAnchor::new(None);
let tee = IdentityAnchor::new(None);
let mut idx = TrustIndex::new();
for i in 0..5 {
idx.insert_grant(make_grant(&grantor, &tee, &format!("read:resource-{i}")));
}
let q = TrustQuery {
limit: Some(2),
..Default::default()
};
let results = query_trust(&idx, &q);
assert_eq!(results.len(), 2);
}
#[test]
fn test_receipt_query_chain_root() {
let anchor = IdentityAnchor::new(None);
let r1 = make_receipt(&anchor, ActionType::Observation, "root observation");
let r2 = ReceiptBuilder::new(
anchor.id(),
ActionType::Decision,
ActionContent::new("decision following observation"),
)
.chain_to(r1.id.clone())
.sign(anchor.signing_key())
.expect("sign r2");
let r3 = ReceiptBuilder::new(
anchor.id(),
ActionType::Mutation,
ActionContent::new("mutation following observation"),
)
.chain_to(r1.id.clone())
.sign(anchor.signing_key())
.expect("sign r3");
let r4 = make_receipt(&anchor, ActionType::Decision, "unrelated decision");
let root_id = r1.id.clone();
let mut idx = ReceiptIndex::new();
idx.insert(r1);
idx.insert(r2);
idx.insert(r3);
idx.insert(r4);
let q = ReceiptQuery {
chain_root: Some(root_id),
..Default::default()
};
let results = query_receipts(&idx, &q);
assert_eq!(results.len(), 2, "expected the two direct successors of r1");
assert!(
results.iter().all(|r| r.previous_receipt.is_some()),
"all results must reference a previous receipt"
);
}
#[test]
fn test_empty_index_queries() {
let receipt_idx = ReceiptIndex::new();
let trust_idx = TrustIndex::new();
let rq = ReceiptQuery::default();
assert!(query_receipts(&receipt_idx, &rq).is_empty());
let tq = TrustQuery::default();
assert!(query_trust(&trust_idx, &tq).is_empty());
}
}