use chrono::{DateTime, Utc};
use git2::{ErrorCode, Repository};
use serde::{Deserialize, Serialize};
use auths_id::error::StorageError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApprovalRequest {
pub request_hash: String,
pub context_summary: String,
pub required_capabilities: Vec<String>,
pub allowed_approvers: Vec<String>,
pub scope: String,
pub ttl_seconds: u64,
pub created_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ConsumedNonce {
expires_at: DateTime<Utc>,
}
fn pending_ref(request_hash: &str) -> String {
format!("refs/auths/approvals/pending/{}", request_hash)
}
fn consumed_ref(jti: &str) -> String {
format!("refs/auths/approvals/consumed/{}", jti)
}
fn write_json_blob(repo: &Repository, ref_name: &str, data: &[u8]) -> Result<(), StorageError> {
let blob_oid = repo.blob(data)?;
let sig = repo.signature()?;
repo.reference(ref_name, blob_oid, true, sig.name().unwrap_or("auths"))?;
Ok(())
}
fn read_json_blob(repo: &Repository, ref_name: &str) -> Result<Option<Vec<u8>>, StorageError> {
match repo.find_reference(ref_name) {
Ok(reference) => {
let oid = reference
.target()
.ok_or_else(|| StorageError::NotFound("ref has no target".into()))?;
let blob = repo.find_blob(oid)?;
Ok(Some(blob.content().to_vec()))
}
Err(e) if e.code() == ErrorCode::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn store_approval_request(
repo: &Repository,
request: &ApprovalRequest,
) -> Result<(), StorageError> {
let json = serde_json::to_vec(request)?;
let ref_name = pending_ref(&request.request_hash);
write_json_blob(repo, &ref_name, &json)
}
pub fn load_approval_request(
repo: &Repository,
request_hash: &str,
) -> Result<Option<ApprovalRequest>, StorageError> {
let ref_name = pending_ref(request_hash);
match read_json_blob(repo, &ref_name)? {
Some(data) => Ok(Some(serde_json::from_slice(&data)?)),
None => Ok(None),
}
}
pub fn list_pending_approvals(
repo: &Repository,
now: DateTime<Utc>,
) -> Result<Vec<ApprovalRequest>, StorageError> {
let mut result = Vec::new();
let refs = repo.references_glob("refs/auths/approvals/pending/*")?;
for reference in refs {
let reference = reference?;
let ref_name = reference.name().unwrap_or_default().to_string();
if let Some(oid) = reference.target()
&& let Ok(blob) = repo.find_blob(oid)
&& let Ok(request) = serde_json::from_slice::<ApprovalRequest>(blob.content())
{
if request.expires_at > now {
result.push(request);
} else {
let _ = repo.find_reference(&ref_name).and_then(|mut r| r.delete());
}
}
}
result.sort_by(|a, b| a.created_at.cmp(&b.created_at));
Ok(result)
}
pub fn mark_nonce_consumed(
repo: &Repository,
jti: &str,
expires_at: DateTime<Utc>,
) -> Result<(), StorageError> {
let nonce = ConsumedNonce { expires_at };
let json = serde_json::to_vec(&nonce)?;
let ref_name = consumed_ref(jti);
write_json_blob(repo, &ref_name, &json)
}
pub fn is_nonce_consumed(repo: &Repository, jti: &str) -> Result<bool, StorageError> {
let ref_name = consumed_ref(jti);
Ok(read_json_blob(repo, &ref_name)?.is_some())
}
pub fn remove_approval_request(repo: &Repository, request_hash: &str) -> Result<(), StorageError> {
let ref_name = pending_ref(request_hash);
match repo.find_reference(&ref_name) {
Ok(mut r) => {
r.delete()?;
Ok(())
}
Err(e) if e.code() == ErrorCode::NotFound => Ok(()),
Err(e) => Err(e.into()),
}
}
pub fn prune_expired_nonces(repo: &Repository, now: DateTime<Utc>) -> Result<usize, StorageError> {
let mut pruned = 0;
let refs = repo.references_glob("refs/auths/approvals/consumed/*")?;
for reference in refs {
let reference = reference?;
let ref_name = reference.name().unwrap_or_default().to_string();
if let Some(oid) = reference.target()
&& let Ok(blob) = repo.find_blob(oid)
&& let Ok(nonce) = serde_json::from_slice::<ConsumedNonce>(blob.content())
&& nonce.expires_at < now
{
let _ = repo.find_reference(&ref_name).and_then(|mut r| r.delete());
pruned += 1;
}
}
Ok(pruned)
}