Skip to main content

tap_node/validation/
agent_validator.rs

1//! Agent authorization validation for transaction responses
2
3use 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
10/// Validator that ensures only authorized agents can respond to transactions
11///
12/// This validator checks that messages responding to a transaction (like Authorize,
13/// Cancel, Reject) are only accepted from agents that are part of the transaction.
14pub struct AgentAuthorizationValidator {
15    storage: Arc<Storage>,
16}
17
18impl AgentAuthorizationValidator {
19    /// Create a new agent authorization validator
20    pub fn new(storage: Arc<Storage>) -> Self {
21        Self { storage }
22    }
23
24    /// Check if a message is a response to a transaction
25    fn is_transaction_response(message: &PlainMessage) -> bool {
26        // Messages that are responses to transactions typically have these types
27        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    /// Extract transaction ID from message
42    async fn get_transaction_id(&self, message: &PlainMessage) -> Option<String> {
43        // First try to get it from thread_id
44        if let Some(thread_id) = &message.thid {
45            // Look up the original transaction by thread ID
46            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        // Try to parse the message and extract transaction_id from specific message types
54        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        // Only validate transaction response messages
73        if !Self::is_transaction_response(message) {
74            return ValidationResult::Accept;
75        }
76
77        // Get the transaction ID
78        let transaction_id = match self.get_transaction_id(message).await {
79            Some(id) => id,
80            None => {
81                // Fail-closed: reject transaction responses without a determinable transaction ID
82                return ValidationResult::Reject(
83                    "Cannot determine transaction ID for transaction response".to_string(),
84                );
85            }
86        };
87
88        // Check if the sender is authorized for this transaction
89        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        // A Connect message is not a transaction response
123        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 => {} // Expected
133            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        // An Authorize message with a transaction_id that doesn't exist in storage
148        // should be rejected because the sender is not authorized
149        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}