use std::sync::Arc;
use std::time::Duration;
use tracing::{info, warn};
use super::grant::{ScopeGrantStore, ScopeStatus};
pub fn spawn_expiry_task(
grant_store: Arc<ScopeGrantStore>,
interval_secs: u64,
) -> tokio::task::JoinHandle<()> {
let interval = Duration::from_secs(interval_secs.max(10));
tokio::spawn(async move {
let mut ticker = tokio::time::interval(interval);
ticker.tick().await;
loop {
ticker.tick().await;
process_expired_grants(&grant_store);
}
})
}
#[derive(Debug, Clone)]
pub struct ScopeEvent {
pub event_type: &'static str,
pub scope_name: String,
pub grantee_type: String,
pub grantee_id: String,
pub detail: String,
}
pub fn process_expired_grants_with_events(store: &ScopeGrantStore) -> Vec<ScopeEvent> {
let mut events = Vec::new();
process_expired_grants_inner(store, &mut events);
events
}
fn process_expired_grants(store: &ScopeGrantStore) {
let _ = process_expired_grants_with_events(store);
}
fn process_expired_grants_inner(store: &ScopeGrantStore, events: &mut Vec<ScopeEvent>) {
let all_grants = store.list(None);
let mut expired_count = 0u32;
let mut grace_count = 0u32;
for grant in &all_grants {
if grant.expires_at == 0 {
continue; }
match grant.status() {
ScopeStatus::Grace => {
grace_count += 1;
info!(
scope = %grant.scope_name,
grantee = %grant.grantee_id,
grantee_type = %grant.grantee_type,
"scope grant in grace period"
);
events.push(ScopeEvent {
event_type: "scope.grace_entered",
scope_name: grant.scope_name.clone(),
grantee_type: grant.grantee_type.clone(),
grantee_id: grant.grantee_id.clone(),
detail: format!("expires_at={}", grant.expires_at),
});
}
ScopeStatus::Expired => {
expired_count += 1;
events.push(ScopeEvent {
event_type: "scope.expired",
scope_name: grant.scope_name.clone(),
grantee_type: grant.grantee_type.clone(),
grantee_id: grant.grantee_id.clone(),
detail: grant.on_expire_action.clone(),
});
execute_on_expire(store, grant);
}
ScopeStatus::Active | ScopeStatus::None => {}
}
}
if expired_count > 0 || grace_count > 0 {
info!(
expired = expired_count,
grace = grace_count,
"scope expiry check completed"
);
}
}
fn execute_on_expire(store: &ScopeGrantStore, grant: &super::grant::ScopeGrant) {
let action = &grant.on_expire_action;
if action.is_empty() {
return;
}
if action == "revoke_all" {
match store.revoke(&grant.scope_name, &grant.grantee_type, &grant.grantee_id) {
Ok(_) => {
info!(
scope = %grant.scope_name,
grantee = %grant.grantee_id,
"expired scope grant revoked (ON EXPIRE REVOKE ALL)"
);
}
Err(e) => {
warn!(
scope = %grant.scope_name,
error = %e,
"failed to revoke expired scope grant"
);
}
}
return;
}
if let Some(downgrade_scope) = action.strip_prefix("grant:") {
match store.grant(
downgrade_scope,
&grant.grantee_type,
&grant.grantee_id,
"system:expiry",
0, 0,
"",
) {
Ok(_) => {
info!(
old_scope = %grant.scope_name,
new_scope = %downgrade_scope,
grantee = %grant.grantee_id,
"expired scope downgraded (ON EXPIRE GRANT)"
);
}
Err(e) => {
warn!(
scope = %grant.scope_name,
downgrade = %downgrade_scope,
error = %e,
"failed to grant downgrade scope on expiry"
);
}
}
let _ = store.revoke(&grant.scope_name, &grant.grantee_type, &grant.grantee_id);
}
}
#[cfg(test)]
mod tests {
use super::super::grant::ScopeGrantStore;
use super::*;
fn now_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[test]
fn expired_grant_with_revoke_all() {
let store = ScopeGrantStore::new();
let past = now_secs() - 100;
store
.grant("pro:all", "org", "acme", "admin", past, 0, "revoke_all")
.unwrap();
assert!(!store.has_scope("u1", &["acme".into()], "pro:all"));
process_expired_grants(&store);
assert_eq!(store.count(), 0);
}
#[test]
fn expired_grant_with_downgrade() {
let store = ScopeGrantStore::new();
let past = now_secs() - 100;
store
.grant(
"pro:all",
"org",
"acme",
"admin",
past,
0,
"grant:free:basic",
)
.unwrap();
process_expired_grants(&store);
assert!(!store.has_scope("u1", &["acme".into()], "pro:all"));
assert!(store.has_scope("u1", &["acme".into()], "free:basic"));
}
#[test]
fn grace_period_still_effective() {
let store = ScopeGrantStore::new();
let past = now_secs() - 10;
store
.grant("pro:all", "org", "acme", "admin", past, 60, "revoke_all")
.unwrap();
assert!(store.has_scope("u1", &["acme".into()], "pro:all"));
process_expired_grants(&store);
assert_eq!(store.count(), 1); }
}