1use std::sync::Arc;
14
15use async_trait::async_trait;
16use http::StatusCode;
17use serde_json::{json, Value};
18use tokio::sync::Mutex as AsyncMutex;
19
20use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
21use fakecloud_persistence::SnapshotStore;
22
23use crate::state::{SharedShieldState, ShieldState};
24
25pub const SHIELD_ACTIONS: &[&str] = &[
26 "AssociateDRTLogBucket",
27 "AssociateDRTRole",
28 "AssociateHealthCheck",
29 "AssociateProactiveEngagementDetails",
30 "CreateProtection",
31 "CreateProtectionGroup",
32 "CreateSubscription",
33 "DeleteProtection",
34 "DeleteProtectionGroup",
35 "DeleteSubscription",
36 "DescribeAttack",
37 "DescribeAttackStatistics",
38 "DescribeDRTAccess",
39 "DescribeEmergencyContactSettings",
40 "DescribeProtection",
41 "DescribeProtectionGroup",
42 "DescribeSubscription",
43 "DisableApplicationLayerAutomaticResponse",
44 "DisableProactiveEngagement",
45 "DisassociateDRTLogBucket",
46 "DisassociateDRTRole",
47 "DisassociateHealthCheck",
48 "EnableApplicationLayerAutomaticResponse",
49 "EnableProactiveEngagement",
50 "GetSubscriptionState",
51 "ListAttacks",
52 "ListProtectionGroups",
53 "ListProtections",
54 "ListResourcesInProtectionGroup",
55 "ListTagsForResource",
56 "TagResource",
57 "UntagResource",
58 "UpdateApplicationLayerAutomaticResponse",
59 "UpdateEmergencyContactSettings",
60 "UpdateProtectionGroup",
61 "UpdateSubscription",
62];
63
64fn is_mutating_action(action: &str) -> bool {
68 const READ_PREFIXES: &[&str] = &["Get", "List", "Describe"];
69 !READ_PREFIXES.iter().any(|p| action.starts_with(p))
70}
71
72pub struct ShieldService {
73 state: SharedShieldState,
74 snapshot_store: Option<Arc<dyn SnapshotStore>>,
75 snapshot_lock: Arc<AsyncMutex<()>>,
76}
77
78impl ShieldService {
79 pub fn new(state: SharedShieldState) -> Self {
80 Self {
81 state,
82 snapshot_store: None,
83 snapshot_lock: Arc::new(AsyncMutex::new(())),
84 }
85 }
86
87 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
88 self.snapshot_store = Some(store);
89 self
90 }
91
92 async fn save(&self) {
93 crate::persistence::save_snapshot(
94 &self.state,
95 self.snapshot_store.clone(),
96 &self.snapshot_lock,
97 )
98 .await;
99 }
100
101 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
103 let store = self.snapshot_store.clone()?;
104 let state = self.state.clone();
105 let lock = self.snapshot_lock.clone();
106 Some(Arc::new(move || {
107 let state = state.clone();
108 let store = store.clone();
109 let lock = lock.clone();
110 Box::pin(async move {
111 crate::persistence::save_snapshot(&state, Some(store), &lock).await;
112 })
113 }))
114 }
115}
116
117#[async_trait]
118impl AwsService for ShieldService {
119 fn service_name(&self) -> &str {
120 "shield"
121 }
122
123 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
124 let action = req.action.clone();
125 if let Err(msg) = crate::validate::validate_input(&action, &req.json_body()) {
128 return Err(invalid_parameter(msg));
129 }
130 let result = self.dispatch(&action, &req);
131 if is_mutating_action(&action)
132 && matches!(result.as_ref(), Ok(resp) if resp.status.is_success())
133 {
134 self.save().await;
135 }
136 result
137 }
138
139 fn supported_actions(&self) -> &[&str] {
140 SHIELD_ACTIONS
141 }
142}
143
144impl ShieldService {
145 #[allow(clippy::too_many_lines)]
146 fn dispatch(&self, action: &str, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
147 match action {
148 "CreateProtection" => self.create_protection(req),
150 "DescribeProtection" => self.describe_protection(req),
151 "DeleteProtection" => self.delete_protection(req),
152 "ListProtections" => self.list_protections(req),
153 "CreateProtectionGroup" => self.create_protection_group(req),
155 "UpdateProtectionGroup" => self.update_protection_group(req),
156 "DeleteProtectionGroup" => self.delete_protection_group(req),
157 "DescribeProtectionGroup" => self.describe_protection_group(req),
158 "ListProtectionGroups" => self.list_protection_groups(req),
159 "ListResourcesInProtectionGroup" => self.list_resources_in_protection_group(req),
160 "CreateSubscription" => self.create_subscription(req),
162 "DescribeSubscription" => self.describe_subscription(req),
163 "UpdateSubscription" => self.update_subscription(req),
164 "DeleteSubscription" => self.delete_subscription(req),
165 "GetSubscriptionState" => self.get_subscription_state(req),
166 "ListAttacks" => self.list_attacks(req),
168 "DescribeAttack" => self.describe_attack(req),
169 "DescribeAttackStatistics" => self.describe_attack_statistics(req),
170 "AssociateDRTRole" => self.associate_drt_role(req),
172 "DisassociateDRTRole" => self.disassociate_drt_role(req),
173 "AssociateDRTLogBucket" => self.associate_drt_log_bucket(req),
174 "DisassociateDRTLogBucket" => self.disassociate_drt_log_bucket(req),
175 "DescribeDRTAccess" => self.describe_drt_access(req),
176 "AssociateHealthCheck" => self.associate_health_check(req),
178 "DisassociateHealthCheck" => self.disassociate_health_check(req),
179 "AssociateProactiveEngagementDetails" => {
181 self.associate_proactive_engagement_details(req)
182 }
183 "EnableProactiveEngagement" => self.enable_proactive_engagement(req),
184 "DisableProactiveEngagement" => self.disable_proactive_engagement(req),
185 "UpdateEmergencyContactSettings" => self.update_emergency_contact_settings(req),
186 "DescribeEmergencyContactSettings" => self.describe_emergency_contact_settings(req),
187 "EnableApplicationLayerAutomaticResponse" => {
189 self.enable_application_layer_automatic_response(req)
190 }
191 "UpdateApplicationLayerAutomaticResponse" => {
192 self.update_application_layer_automatic_response(req)
193 }
194 "DisableApplicationLayerAutomaticResponse" => {
195 self.disable_application_layer_automatic_response(req)
196 }
197 "TagResource" => self.tag_resource(req),
199 "UntagResource" => self.untag_resource(req),
200 "ListTagsForResource" => self.list_tags_for_resource(req),
201 _ => Err(AwsServiceError::action_not_implemented(
202 self.service_name(),
203 action,
204 )),
205 }
206 }
207}
208
209fn err(code: &str, msg: impl Into<String>) -> AwsServiceError {
212 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, code, msg.into())
213}
214
215pub(crate) fn invalid_parameter(msg: impl Into<String>) -> AwsServiceError {
216 err("InvalidParameterException", msg)
217}
218
219pub(crate) fn resource_not_found(msg: impl Into<String>) -> AwsServiceError {
220 err("ResourceNotFoundException", msg)
221}
222
223pub(crate) fn resource_already_exists(msg: impl Into<String>) -> AwsServiceError {
224 err("ResourceAlreadyExistsException", msg)
225}
226
227pub(crate) fn invalid_resource(msg: impl Into<String>) -> AwsServiceError {
228 err("InvalidResourceException", msg)
229}
230
231pub(crate) fn limits_exceeded(msg: impl Into<String>) -> AwsServiceError {
232 err("LimitsExceededException", msg)
233}
234
235pub(crate) fn no_associated_role(msg: impl Into<String>) -> AwsServiceError {
236 err("NoAssociatedRoleException", msg)
237}
238
239pub(crate) fn locked_subscription(msg: impl Into<String>) -> AwsServiceError {
240 err("LockedSubscriptionException", msg)
241}
242
243pub(crate) fn ok(value: Value) -> Result<AwsResponse, AwsServiceError> {
244 Ok(AwsResponse::ok_json(value))
245}
246
247pub(crate) fn now_epoch() -> f64 {
249 chrono::Utc::now().timestamp_millis() as f64 / 1000.0
250}
251
252pub(crate) const ONE_YEAR_SECS: f64 = 365.0 * 24.0 * 60.0 * 60.0;
254
255pub(crate) fn protection_arn(account: &str, id: &str) -> String {
257 format!("arn:aws:shield::{account}:protection/{id}")
258}
259
260pub(crate) fn protection_group_arn(account: &str, id: &str) -> String {
261 format!("arn:aws:shield::{account}:protection-group/{id}")
262}
263
264pub(crate) fn subscription_arn(account: &str) -> String {
265 format!("arn:aws:shield::{account}:subscription")
266}
267
268impl ShieldService {
269 pub(crate) fn with_account_mut<R>(
271 &self,
272 req: &AwsRequest,
273 f: impl FnOnce(&mut ShieldState) -> R,
274 ) -> R {
275 let mut guard = self.state.write();
276 let acct = guard.get_or_create(&req.account_id);
277 f(acct)
278 }
279
280 pub(crate) fn with_account<R>(&self, req: &AwsRequest, f: impl FnOnce(&ShieldState) -> R) -> R {
283 let guard = self.state.read();
284 match guard.get(&req.account_id) {
285 Some(acct) => f(acct),
286 None => f(&ShieldState::default()),
287 }
288 }
289}
290
291include!("handlers.rs");