tap_node/validation/
agent_validator.rs1use super::{MessageValidator, ValidationResult};
4use crate::storage::Storage;
5use async_trait::async_trait;
6use std::sync::Arc;
7use tap_msg::didcomm::PlainMessage;
8use tap_msg::message::TapMessage;
9
10pub struct AgentAuthorizationValidator {
15 storage: Arc<Storage>,
16}
17
18impl AgentAuthorizationValidator {
19 pub fn new(storage: Arc<Storage>) -> Self {
21 Self { storage }
22 }
23
24 fn is_transaction_response(message: &PlainMessage) -> bool {
26 matches!(
28 message.type_.as_str(),
29 "https://tap.rsvp/schema/1.0#Authorize"
30 | "https://tap.rsvp/schema/1.0#Cancel"
31 | "https://tap.rsvp/schema/1.0#Reject"
32 | "https://tap.rsvp/schema/1.0#Settle"
33 | "https://tap.rsvp/schema/1.0#Revert"
34 | "https://tap.rsvp/schema/1.0#AddAgents"
35 | "https://tap.rsvp/schema/1.0#RemoveAgent"
36 | "https://tap.rsvp/schema/1.0#ReplaceAgent"
37 | "https://tap.rsvp/schema/1.0#UpdatePolicies"
38 )
39 }
40
41 async fn get_transaction_id(&self, message: &PlainMessage) -> Option<String> {
43 if let Some(thread_id) = &message.thid {
45 if let Ok(Some(transaction)) =
47 self.storage.get_transaction_by_thread_id(thread_id).await
48 {
49 return Some(transaction.reference_id);
50 }
51 }
52
53 if let Ok(tap_message) = TapMessage::from_plain_message(message) {
55 match tap_message {
56 TapMessage::Authorize(auth) => Some(auth.transaction_id),
57 TapMessage::Cancel(cancel) => Some(cancel.transaction_id),
58 TapMessage::Reject(reject) => Some(reject.transaction_id),
59 TapMessage::Settle(settle) => Some(settle.transaction_id),
60 TapMessage::Revert(revert) => Some(revert.transaction_id),
61 _ => None,
62 }
63 } else {
64 None
65 }
66 }
67}
68
69#[async_trait]
70impl MessageValidator for AgentAuthorizationValidator {
71 async fn validate(&self, message: &PlainMessage) -> ValidationResult {
72 if !Self::is_transaction_response(message) {
74 return ValidationResult::Accept;
75 }
76
77 let transaction_id = match self.get_transaction_id(message).await {
79 Some(id) => id,
80 None => {
81 return ValidationResult::Reject(
83 "Cannot determine transaction ID for transaction response".to_string(),
84 );
85 }
86 };
87
88 match self
90 .storage
91 .is_agent_authorized_for_transaction(&transaction_id, &message.from)
92 .await
93 {
94 Ok(true) => ValidationResult::Accept,
95 Ok(false) => ValidationResult::Reject(format!(
96 "Agent {} is not authorized to respond to transaction {}",
97 message.from, transaction_id
98 )),
99 Err(e) => {
100 ValidationResult::Reject(format!("Unable to verify agent authorization: {}", e))
101 }
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use tap_msg::message::Authorize;
110 use tempfile::tempdir;
111
112 #[tokio::test]
113 async fn test_non_transaction_response_accepted() {
114 let dir = tempdir().unwrap();
115 let storage = Arc::new(
116 Storage::new(Some(dir.path().join("test.db")))
117 .await
118 .unwrap(),
119 );
120 let validator = AgentAuthorizationValidator::new(storage);
121
122 let message = PlainMessage::new(
124 "test_msg_1".to_string(),
125 "https://tap.rsvp/schema/1.0#Connect".to_string(),
126 serde_json::json!({}),
127 "did:example:sender".to_string(),
128 )
129 .with_recipient("did:example:receiver");
130
131 match validator.validate(&message).await {
132 ValidationResult::Accept => {} ValidationResult::Reject(reason) => panic!("Expected accept, got reject: {}", reason),
134 }
135 }
136
137 #[tokio::test]
138 async fn test_authorize_for_new_transaction_rejected() {
139 let dir = tempdir().unwrap();
140 let storage = Arc::new(
141 Storage::new(Some(dir.path().join("test.db")))
142 .await
143 .unwrap(),
144 );
145 let validator = AgentAuthorizationValidator::new(storage);
146
147 let authorize = Authorize {
150 transaction_id: "new_transaction_123".to_string(),
151 settlement_address: None,
152 expiry: None,
153 };
154
155 let message = PlainMessage::new(
156 "test_msg_2".to_string(),
157 "https://tap.rsvp/schema/1.0#Authorize".to_string(),
158 serde_json::to_value(&authorize).unwrap(),
159 "did:example:sender".to_string(),
160 )
161 .with_recipient("did:example:receiver");
162
163 match validator.validate(&message).await {
164 ValidationResult::Accept => panic!("Expected reject, got accept"),
165 ValidationResult::Reject(reason) => {
166 assert!(reason.contains("not authorized"));
167 }
168 }
169 }
170}