fakecloud_eventbridge/
resource_policy.rs1use std::sync::Arc;
16
17use fakecloud_core::auth::ResourcePolicyProvider;
18
19use crate::state::SharedEventBridgeState;
20
21pub struct EventBridgeResourcePolicyProvider {
25 state: SharedEventBridgeState,
26}
27
28impl EventBridgeResourcePolicyProvider {
29 pub fn new(state: SharedEventBridgeState) -> Self {
30 Self { state }
31 }
32
33 pub fn shared(state: SharedEventBridgeState) -> Arc<dyn ResourcePolicyProvider> {
34 Arc::new(Self::new(state))
35 }
36}
37
38impl ResourcePolicyProvider for EventBridgeResourcePolicyProvider {
39 fn resource_policy(&self, service: &str, resource_arn: &str) -> Option<String> {
40 if !service.eq_ignore_ascii_case("events") {
41 return None;
42 }
43 if !is_event_bus_arn(resource_arn) {
44 return None;
45 }
46 let accts = self.state.read();
47 let acct = resource_arn.split(':').nth(4).unwrap_or("");
48 let state = accts.get(acct).unwrap_or_else(|| accts.default_ref());
49 state
50 .buses
51 .values()
52 .find(|b| b.arn == resource_arn)
53 .and_then(|b| b.policy.as_ref())
54 .map(|p| p.to_string())
55 }
56}
57
58fn is_event_bus_arn(arn: &str) -> bool {
61 let Some(rest) = arn.strip_prefix("arn:aws:events:") else {
62 return false;
63 };
64 let parts: Vec<&str> = rest.splitn(3, ':').collect();
65 if parts.len() != 3 {
66 return false;
67 }
68 if parts[0].is_empty() || parts[1].is_empty() {
69 return false;
70 }
71 let resource = parts[2];
72 if let Some(name) = resource.strip_prefix("event-bus/") {
73 !name.is_empty()
74 } else {
75 false
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::state::{EventBridgeState, EventBus};
83 use chrono::Utc;
84 use fakecloud_core::multi_account::MultiAccountState;
85 use parking_lot::RwLock;
86 use serde_json::json;
87 use std::collections::BTreeMap;
88
89 fn state_with_bus(arn: &str, policy: Option<serde_json::Value>) -> SharedEventBridgeState {
90 let state = Arc::new(RwLock::new(MultiAccountState::<EventBridgeState>::new(
91 "123456789012",
92 "us-east-1",
93 "http://localhost:4566",
94 )));
95 let name = arn
96 .rsplit_once("event-bus/")
97 .map(|(_, n)| n)
98 .unwrap_or("default")
99 .to_string();
100 state.write().default_mut().buses.insert(
101 name.clone(),
102 EventBus {
103 name: name.clone(),
104 arn: arn.to_string(),
105 description: None,
106 policy,
107 tags: BTreeMap::new(),
108 creation_time: Utc::now(),
109 last_modified_time: Utc::now(),
110 kms_key_identifier: None,
111 dead_letter_config: None,
112 },
113 );
114 state
115 }
116
117 #[test]
118 fn returns_stored_policy_for_event_bus_arn() {
119 let policy = json!({"Version":"2012-10-17","Statement":[]});
120 let arn = "arn:aws:events:us-east-1:123456789012:event-bus/my-bus";
121 let state = state_with_bus(arn, Some(policy.clone()));
122 let provider = EventBridgeResourcePolicyProvider::new(state);
123 let raw = provider.resource_policy("events", arn).unwrap();
124 let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap();
125 assert_eq!(parsed, policy);
126 }
127
128 #[test]
129 fn returns_none_when_bus_has_no_policy() {
130 let arn = "arn:aws:events:us-east-1:123456789012:event-bus/my-bus";
131 let state = state_with_bus(arn, None);
132 let provider = EventBridgeResourcePolicyProvider::new(state);
133 assert_eq!(provider.resource_policy("events", arn), None);
134 }
135
136 #[test]
137 fn returns_none_when_bus_missing() {
138 let arn = "arn:aws:events:us-east-1:123456789012:event-bus/other";
139 let state = state_with_bus(arn, Some(json!({})));
140 let provider = EventBridgeResourcePolicyProvider::new(state);
141 assert_eq!(
142 provider.resource_policy(
143 "events",
144 "arn:aws:events:us-east-1:123456789012:event-bus/my-bus"
145 ),
146 None
147 );
148 }
149
150 #[test]
151 fn returns_none_for_non_events_service_prefix() {
152 let arn = "arn:aws:events:us-east-1:123456789012:event-bus/b";
153 let state = state_with_bus(arn, Some(json!({})));
154 let provider = EventBridgeResourcePolicyProvider::new(state);
155 assert_eq!(provider.resource_policy("sns", arn), None);
156 assert_eq!(provider.resource_policy("sqs", arn), None);
157 }
158
159 #[test]
160 fn service_prefix_match_is_case_insensitive() {
161 let arn = "arn:aws:events:us-east-1:123456789012:event-bus/b";
162 let state = state_with_bus(arn, Some(json!({})));
163 let provider = EventBridgeResourcePolicyProvider::new(state);
164 assert!(provider.resource_policy("EVENTS", arn).is_some());
165 }
166
167 #[test]
168 fn returns_none_for_malformed_arn() {
169 let arn = "arn:aws:events:us-east-1:123456789012:event-bus/b";
170 let state = state_with_bus(arn, Some(json!({})));
171 let provider = EventBridgeResourcePolicyProvider::new(state);
172 assert_eq!(provider.resource_policy("events", ""), None);
173 assert_eq!(provider.resource_policy("events", "not-an-arn"), None);
174 assert_eq!(provider.resource_policy("events", "arn:aws:events:"), None);
175 assert_eq!(
176 provider.resource_policy(
177 "events",
178 "arn:aws:events:us-east-1:123456789012:rule/my-rule"
179 ),
180 None
181 );
182 }
183
184 #[test]
185 fn is_event_bus_arn_rejects_empty_segments() {
186 assert!(!is_event_bus_arn("arn:aws:events:::event-bus/b"));
187 assert!(!is_event_bus_arn("arn:aws:events:us-east-1::event-bus/b"));
188 assert!(!is_event_bus_arn(
189 "arn:aws:events:us-east-1:123456789012:event-bus/"
190 ));
191 }
192
193 #[test]
194 fn shared_constructor_wraps_in_arc() {
195 let arn = "arn:aws:events:us-east-1:123456789012:event-bus/b";
196 let state = state_with_bus(arn, Some(json!({"x": 1})));
197 let arc = EventBridgeResourcePolicyProvider::shared(state);
198 let raw = arc.resource_policy("events", arn).unwrap();
199 let parsed: serde_json::Value = serde_json::from_str(&raw).unwrap();
200 assert_eq!(parsed, json!({"x": 1}));
201 }
202}