use std::sync::Arc;
use async_trait::async_trait;
use super::types::{GroupedPermissions, PermissionRequest, PermissionResponse, PermissionType};
use crate::WalletError;
#[async_trait]
pub trait PermissionCallbacks: Send + Sync {
async fn on_protocol_permission_requested(
&self,
request: &PermissionRequest,
) -> Result<PermissionResponse, WalletError>;
async fn on_basket_access_requested(
&self,
request: &PermissionRequest,
) -> Result<PermissionResponse, WalletError>;
async fn on_certificate_access_requested(
&self,
request: &PermissionRequest,
) -> Result<PermissionResponse, WalletError>;
async fn on_spending_authorization_requested(
&self,
request: &PermissionRequest,
) -> Result<PermissionResponse, WalletError>;
async fn on_grouped_permission_requested(
&self,
grouped: &GroupedPermissions,
originator: &str,
) -> Result<Vec<PermissionResponse>, WalletError>;
async fn on_counterparty_permission_requested(
&self,
request: &PermissionRequest,
) -> Result<PermissionResponse, WalletError>;
}
pub async fn fire_permission_callback(
callbacks: &Option<Arc<dyn PermissionCallbacks>>,
request: &PermissionRequest,
) -> Result<PermissionResponse, WalletError> {
let cbs = match callbacks {
Some(c) => c,
None => {
return Ok(PermissionResponse::Deny {
reason: "No permission callback registered".to_string(),
});
}
};
let result = match request.permission_type {
PermissionType::ProtocolPermission => cbs.on_protocol_permission_requested(request).await,
PermissionType::BasketAccess => cbs.on_basket_access_requested(request).await,
PermissionType::CertificateAccess => cbs.on_certificate_access_requested(request).await,
PermissionType::SpendingAuthorization => {
cbs.on_spending_authorization_requested(request).await
}
};
match result {
Ok(resp) => Ok(resp),
Err(e) => {
tracing::warn!("Permission callback error (swallowed): {}", e);
Ok(PermissionResponse::Deny {
reason: format!("Callback error: {}", e),
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct AlwaysGrantCallbacks;
#[async_trait]
impl PermissionCallbacks for AlwaysGrantCallbacks {
async fn on_protocol_permission_requested(
&self,
_request: &PermissionRequest,
) -> Result<PermissionResponse, WalletError> {
Ok(PermissionResponse::Grant { expiry: None })
}
async fn on_basket_access_requested(
&self,
_request: &PermissionRequest,
) -> Result<PermissionResponse, WalletError> {
Ok(PermissionResponse::Grant { expiry: None })
}
async fn on_certificate_access_requested(
&self,
_request: &PermissionRequest,
) -> Result<PermissionResponse, WalletError> {
Ok(PermissionResponse::Grant { expiry: None })
}
async fn on_spending_authorization_requested(
&self,
_request: &PermissionRequest,
) -> Result<PermissionResponse, WalletError> {
Ok(PermissionResponse::Grant { expiry: None })
}
async fn on_grouped_permission_requested(
&self,
_grouped: &GroupedPermissions,
_originator: &str,
) -> Result<Vec<PermissionResponse>, WalletError> {
Ok(vec![PermissionResponse::Grant { expiry: None }])
}
async fn on_counterparty_permission_requested(
&self,
_request: &PermissionRequest,
) -> Result<PermissionResponse, WalletError> {
Ok(PermissionResponse::Grant { expiry: None })
}
}
fn make_request(ptype: PermissionType) -> PermissionRequest {
PermissionRequest {
permission_type: ptype,
originator: "test.example.com".to_string(),
privileged: None,
protocol: Some("test-proto".to_string()),
security_level: Some(1),
counterparty: None,
basket_name: None,
cert_type: None,
cert_fields: None,
verifier: None,
amount: None,
description: None,
labels: None,
is_new_user: false,
}
}
#[tokio::test]
async fn test_fire_callback_no_callbacks() {
let req = make_request(PermissionType::ProtocolPermission);
let resp = fire_permission_callback(&None, &req).await.unwrap();
match resp {
PermissionResponse::Deny { reason } => {
assert!(reason.contains("No permission callback registered"));
}
_ => panic!("Expected Deny when no callbacks registered"),
}
}
#[tokio::test]
async fn test_fire_callback_dispatches_by_type() {
let cbs: Option<Arc<dyn PermissionCallbacks>> = Some(Arc::new(AlwaysGrantCallbacks));
for ptype in [
PermissionType::ProtocolPermission,
PermissionType::BasketAccess,
PermissionType::CertificateAccess,
PermissionType::SpendingAuthorization,
] {
let req = make_request(ptype);
let resp = fire_permission_callback(&cbs, &req).await.unwrap();
assert!(matches!(resp, PermissionResponse::Grant { .. }));
}
}
#[tokio::test]
async fn test_fire_callback_swallows_errors() {
struct FailingCallbacks;
#[async_trait]
impl PermissionCallbacks for FailingCallbacks {
async fn on_protocol_permission_requested(
&self,
_request: &PermissionRequest,
) -> Result<PermissionResponse, WalletError> {
Err(WalletError::Internal("intentional test failure".into()))
}
async fn on_basket_access_requested(
&self,
_request: &PermissionRequest,
) -> Result<PermissionResponse, WalletError> {
Err(WalletError::Internal("fail".into()))
}
async fn on_certificate_access_requested(
&self,
_request: &PermissionRequest,
) -> Result<PermissionResponse, WalletError> {
Err(WalletError::Internal("fail".into()))
}
async fn on_spending_authorization_requested(
&self,
_request: &PermissionRequest,
) -> Result<PermissionResponse, WalletError> {
Err(WalletError::Internal("fail".into()))
}
async fn on_grouped_permission_requested(
&self,
_grouped: &GroupedPermissions,
_originator: &str,
) -> Result<Vec<PermissionResponse>, WalletError> {
Err(WalletError::Internal("fail".into()))
}
async fn on_counterparty_permission_requested(
&self,
_request: &PermissionRequest,
) -> Result<PermissionResponse, WalletError> {
Err(WalletError::Internal("fail".into()))
}
}
let cbs: Option<Arc<dyn PermissionCallbacks>> = Some(Arc::new(FailingCallbacks));
let req = make_request(PermissionType::ProtocolPermission);
let resp = fire_permission_callback(&cbs, &req).await.unwrap();
assert!(matches!(resp, PermissionResponse::Deny { .. }));
}
}