Skip to main content

fakecloud_eventbridge/
resource_policy.rs

1//! EventBridge implementation of [`ResourcePolicyProvider`].
2//!
3//! EventBridge persists per-bus access policies as JSON on
4//! [`crate::state::EventBus::policy`]. `PutPermission`, `RemovePermission`,
5//! and direct `policy` writes via `PutPermission` all settle into that
6//! single slot. This file is the read-side bridge that dispatch consults
7//! via `evaluate_with_resource_policy`, so cross-account `PutEvents`
8//! callers go through Allow-union and explicit-Deny enforcement against
9//! the bus's policy in addition to identity policies and SCPs.
10//!
11//! Mirrors `fakecloud_sns::resource_policy` intentionally: single-service
12//! gate, ARN parsing, state lookup, return `None` for anything not owned
13//! here so providers compose safely.
14
15use std::sync::Arc;
16
17use fakecloud_core::auth::ResourcePolicyProvider;
18
19use crate::state::SharedEventBridgeState;
20
21/// Concrete [`ResourcePolicyProvider`] backed by the in-memory
22/// EventBridge state. Server bootstrap clone-shares it via
23/// [`fakecloud_core::auth::MultiResourcePolicyProvider`].
24pub 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
58/// Light validity check on an EventBridge bus ARN. Accepts the
59/// `arn:aws:events:REGION:ACCOUNT:event-bus/NAME` shape and nothing else.
60fn 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}