use crate::atp::policy::{Capability, CapabilityError};
use crate::net::atp::protocol::PeerId;
use crate::types::outcome::Outcome;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::time::SystemTime;
pub mod manager;
pub mod pairing;
pub mod storage;
pub use manager::GrantManager;
pub use pairing::{PairingCode, PairingFlow, PairingManager};
pub use storage::{GrantRecord, GrantStorage};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum GrantOperation {
Issued,
Received,
Revoked,
Rotated,
Used,
Delegated,
}
impl std::fmt::Display for GrantOperation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Issued => write!(f, "issued"),
Self::Received => write!(f, "received"),
Self::Revoked => write!(f, "revoked"),
Self::Rotated => write!(f, "rotated"),
Self::Used => write!(f, "used"),
Self::Delegated => write!(f, "delegated"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrantAuditRecord {
pub grant_id: String,
pub operation: GrantOperation,
pub actor: PeerId,
pub target: Option<PeerId>,
pub timestamp: SystemTime,
pub context: HashMap<String, String>,
pub capability_summary: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum GrantState {
Pending,
Active,
Revoked,
Expired,
Rotated,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrantInfo {
pub capability: Capability,
pub state: GrantState,
pub created_at: SystemTime,
pub last_used: Option<SystemTime>,
pub usage_count: u64,
pub parent_grant_id: Option<String>,
pub child_grant_ids: HashSet<String>,
pub metadata: HashMap<String, String>,
}
impl GrantInfo {
#[must_use]
pub fn new(capability: Capability) -> Self {
Self {
capability,
state: GrantState::Active,
created_at: SystemTime::now(),
last_used: None,
usage_count: 0,
parent_grant_id: None,
child_grant_ids: HashSet::new(),
metadata: HashMap::new(),
}
}
#[must_use]
pub fn is_usable(&self) -> bool {
match self.state {
GrantState::Active => self.capability.is_valid(self.usage_count),
_ => false,
}
}
pub fn record_usage(&mut self) {
self.usage_count = self.usage_count.saturating_add(1);
self.last_used = Some(SystemTime::now());
if let Some(max_uses) = self.capability.temporal.max_uses {
if self.usage_count >= max_uses {
self.state = GrantState::Expired;
}
}
}
pub fn revoke(&mut self) {
self.state = GrantState::Revoked;
}
pub fn rotate(&mut self, new_grant_id: String) {
self.state = GrantState::Rotated;
self.metadata.insert("rotated_to".to_string(), new_grant_id);
}
#[must_use]
pub fn redacted_summary(&self) -> String {
let actions: Vec<String> = self
.capability
.actions
.iter()
.map(|a| format!("{a:?}"))
.collect();
let scope_type = match &self.capability.scope {
crate::atp::policy::ResourceScope::Any => "any",
crate::atp::policy::ResourceScope::Object(_) => "object",
crate::atp::policy::ResourceScope::Path(_) => "path",
crate::atp::policy::ResourceScope::Inbox => "inbox",
crate::atp::policy::ResourceScope::Team(_) => "team",
crate::atp::policy::ResourceScope::Relay { .. } => "relay",
crate::atp::policy::ResourceScope::Cache { .. } => "cache",
};
format!(
"grant={} scope={} actions=[{}] state={:?} uses={}/{}",
&self.capability.grant_id[..8.min(self.capability.grant_id.len())],
scope_type,
actions.join(","),
self.state,
self.usage_count,
self.capability
.temporal
.max_uses
.map_or("∞".to_string(), |u| u.to_string())
)
}
}
pub type GrantResult<T> = Outcome<T, GrantError>;
#[derive(Debug, thiserror::Error)]
pub enum GrantError {
#[error("grant not found: {grant_id}")]
NotFound { grant_id: String },
#[error("grant already exists: {grant_id}")]
AlreadyExists { grant_id: String },
#[error("invalid grant state {state:?} for operation")]
InvalidState { state: GrantState },
#[error("permission denied: {reason}")]
PermissionDenied { reason: String },
#[error("capability error: {0}")]
Capability(#[from] CapabilityError),
#[error("storage error: {0}")]
Storage(String),
#[error("validation failed: {issues:?}")]
ValidationFailed { issues: Vec<String> },
#[error("pairing error: {reason}")]
PairingError { reason: String },
}
#[derive(Debug, Clone)]
pub struct CreateGrantRequest {
pub subject: PeerId,
pub scope: crate::atp::policy::ResourceScope,
pub actions: HashSet<crate::atp::policy::CapabilityAction>,
pub temporal: crate::atp::policy::TemporalScope,
pub constraints: crate::atp::policy::ScopeConstraints,
pub description: Option<String>,
pub parent_grant_id: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct GrantQuery {
pub subject: Option<PeerId>,
pub issuer: Option<PeerId>,
pub state: Option<GrantState>,
pub action: Option<crate::atp::policy::CapabilityAction>,
pub usable_only: bool,
pub limit: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrantStats {
pub total_grants: u64,
pub grants_by_state: HashMap<GrantState, u64>,
pub total_usage: u64,
pub unique_subjects: u64,
pub unique_issuers: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrantTemplate {
pub name: String,
pub description: String,
pub scope: crate::atp::policy::ResourceScope,
pub actions: HashSet<crate::atp::policy::CapabilityAction>,
pub temporal: crate::atp::policy::TemporalScope,
pub constraints: crate::atp::policy::ScopeConstraints,
}
impl GrantTemplate {
#[must_use]
pub fn read_once() -> Self {
use crate::atp::policy::{
CapabilityAction, ResourceScope, ScopeConstraints, TemporalScope,
};
let mut actions = HashSet::new();
actions.insert(CapabilityAction::ReadOnce);
Self {
name: "read-once".to_string(),
description: "Single-use read access".to_string(),
scope: ResourceScope::Any,
actions,
temporal: TemporalScope::once(),
constraints: ScopeConstraints::default(),
}
}
#[must_use]
pub fn share_24h() -> Self {
use crate::atp::policy::{
CapabilityAction, ResourceScope, ScopeConstraints, TemporalScope,
};
let mut actions = HashSet::new();
actions.insert(CapabilityAction::Share);
Self {
name: "share-24h".to_string(),
description: "24-hour sharing capability".to_string(),
scope: ResourceScope::Any,
actions,
temporal: TemporalScope::expires_in(std::time::Duration::from_secs(24 * 3600)),
constraints: ScopeConstraints::default(),
}
}
#[must_use]
pub fn inbox_write() -> Self {
use crate::atp::policy::{
CapabilityAction, ResourceScope, ScopeConstraints, TemporalScope,
};
let mut actions = HashSet::new();
actions.insert(CapabilityAction::WriteInbox);
Self {
name: "inbox-write".to_string(),
description: "Write access to inbox".to_string(),
scope: ResourceScope::Inbox,
actions,
temporal: TemporalScope::expires_in(std::time::Duration::from_secs(7 * 24 * 3600)), constraints: ScopeConstraints::default(),
}
}
#[must_use]
pub fn team_read(team: String) -> Self {
use crate::atp::policy::{
CapabilityAction, ResourceScope, ScopeConstraints, TemporalScope,
};
let mut actions = HashSet::new();
actions.insert(CapabilityAction::Read);
Self {
name: format!("team-{}-read", team),
description: format!("Read access to team {} resources", team),
scope: ResourceScope::Team(team),
actions,
temporal: TemporalScope::expires_in(std::time::Duration::from_secs(30 * 24 * 3600)), constraints: ScopeConstraints::default(),
}
}
pub fn apply_to_request(&self, request: &mut CreateGrantRequest) {
request.scope = self.scope.clone();
request.actions.clone_from(&self.actions);
request.temporal = self.temporal.clone();
request.constraints = self.constraints.clone();
if request.description.is_none() {
request.description = Some(self.description.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::atp::policy::{CapabilityAction, ResourceScope, ScopeConstraints, TemporalScope};
use crate::net::atp::protocol::PeerId;
use std::collections::HashSet;
use std::time::Duration;
#[test]
fn grant_info_tracks_usage() {
let mut actions = HashSet::new();
actions.insert(CapabilityAction::Read);
let capability = crate::atp::policy::Capability::new(
"test-grant".to_string(),
PeerId::test(1),
PeerId::test(2),
ResourceScope::Any,
actions,
TemporalScope::once(),
ScopeConstraints::default(),
);
let mut grant_info = GrantInfo::new(capability);
assert!(grant_info.is_usable());
assert_eq!(grant_info.usage_count, 0);
grant_info.record_usage();
assert_eq!(grant_info.usage_count, 1);
assert_eq!(grant_info.state, GrantState::Expired); assert!(!grant_info.is_usable());
}
#[test]
fn grant_info_usage_count_saturates_at_u64_max() {
let mut actions = HashSet::new();
actions.insert(CapabilityAction::Read);
let capability = crate::atp::policy::Capability::new(
"saturating-grant".to_string(),
PeerId::test(1),
PeerId::test(2),
ResourceScope::Any,
actions,
TemporalScope::expires_in(Duration::from_secs(3600)),
ScopeConstraints::default(),
);
let mut grant_info = GrantInfo::new(capability);
grant_info.usage_count = u64::MAX;
grant_info.record_usage();
assert_eq!(grant_info.usage_count, u64::MAX);
assert_eq!(grant_info.state, GrantState::Active);
}
#[test]
fn grant_templates_create_common_patterns() {
let read_once = GrantTemplate::read_once();
assert_eq!(read_once.name, "read-once");
assert!(read_once.actions.contains(&CapabilityAction::ReadOnce));
assert_eq!(read_once.temporal.max_uses, Some(1));
let share_24h = GrantTemplate::share_24h();
assert_eq!(share_24h.name, "share-24h");
assert!(share_24h.actions.contains(&CapabilityAction::Share));
let inbox = GrantTemplate::inbox_write();
assert_eq!(inbox.name, "inbox-write");
assert!(inbox.actions.contains(&CapabilityAction::WriteInbox));
assert!(matches!(inbox.scope, ResourceScope::Inbox));
let team = GrantTemplate::team_read("engineering".to_string());
assert_eq!(team.name, "team-engineering-read");
assert!(team.actions.contains(&CapabilityAction::Read));
assert!(matches!(team.scope, ResourceScope::Team(ref t) if t == "engineering"));
}
#[test]
fn grant_redacted_summary_includes_key_info() {
let mut actions = HashSet::new();
actions.insert(CapabilityAction::Read);
actions.insert(CapabilityAction::Share);
let capability = crate::atp::policy::Capability::new(
"test-grant-12345".to_string(),
PeerId::test(1),
PeerId::test(2),
ResourceScope::Inbox,
actions,
TemporalScope::expires_in(Duration::from_secs(3600)),
ScopeConstraints::default(),
);
let grant_info = GrantInfo::new(capability);
let summary = grant_info.redacted_summary();
assert!(summary.contains("grant=test-gra"));
assert!(summary.contains("scope=inbox"));
assert!(summary.contains("Read"));
assert!(summary.contains("Share"));
assert!(summary.contains("state=Active"));
}
#[test]
fn grant_query_provides_filtering() {
let query = GrantQuery {
subject: Some(PeerId::test(1)),
state: Some(GrantState::Active),
usable_only: true,
limit: Some(10),
..Default::default()
};
assert_eq!(query.subject, Some(PeerId::test(1)));
assert_eq!(query.state, Some(GrantState::Active));
assert!(query.usable_only);
assert_eq!(query.limit, Some(10));
}
}