Skip to main content

fakecloud_cloudtrail/
service.rs

1//! AWS CloudTrail (`cloudtrail`) awsJson1.1 dispatch + operation handlers.
2//!
3//! The full 60-operation CloudTrail control plane. State is account-partitioned
4//! and persisted. Each resource is stored as its already-output-valid JSON
5//! object so `Get`/`Describe` echoes exactly what `Create`/`Update` persisted.
6
7use std::sync::Arc;
8
9use async_trait::async_trait;
10use http::StatusCode;
11use serde_json::{json, Map, Value};
12use tokio::sync::Mutex as AsyncMutex;
13use uuid::Uuid;
14
15use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
16use fakecloud_persistence::SnapshotStore;
17
18use crate::persistence::save_snapshot;
19use crate::state::{CloudTrailData, SharedCloudTrailState};
20use crate::validate::validate_input;
21
22#[cfg(test)]
23mod tests;
24
25/// Every operation name in the CloudTrail Smithy model (60 operations).
26pub const CLOUDTRAIL_ACTIONS: &[&str] = &[
27    "AddTags",
28    "CancelQuery",
29    "CreateChannel",
30    "CreateDashboard",
31    "CreateEventDataStore",
32    "CreateTrail",
33    "DeleteChannel",
34    "DeleteDashboard",
35    "DeleteEventDataStore",
36    "DeleteResourcePolicy",
37    "DeleteTrail",
38    "DeregisterOrganizationDelegatedAdmin",
39    "DescribeQuery",
40    "DescribeTrails",
41    "DisableFederation",
42    "EnableFederation",
43    "GenerateQuery",
44    "GetChannel",
45    "GetDashboard",
46    "GetEventConfiguration",
47    "GetEventDataStore",
48    "GetEventSelectors",
49    "GetImport",
50    "GetInsightSelectors",
51    "GetQueryResults",
52    "GetResourcePolicy",
53    "GetTrail",
54    "GetTrailStatus",
55    "ListChannels",
56    "ListDashboards",
57    "ListEventDataStores",
58    "ListImportFailures",
59    "ListImports",
60    "ListInsightsData",
61    "ListInsightsMetricData",
62    "ListPublicKeys",
63    "ListQueries",
64    "ListTags",
65    "ListTrails",
66    "LookupEvents",
67    "PutEventConfiguration",
68    "PutEventSelectors",
69    "PutInsightSelectors",
70    "PutResourcePolicy",
71    "RegisterOrganizationDelegatedAdmin",
72    "RemoveTags",
73    "RestoreEventDataStore",
74    "SearchSampleQueries",
75    "StartDashboardRefresh",
76    "StartEventDataStoreIngestion",
77    "StartImport",
78    "StartLogging",
79    "StartQuery",
80    "StopEventDataStoreIngestion",
81    "StopImport",
82    "StopLogging",
83    "UpdateChannel",
84    "UpdateDashboard",
85    "UpdateEventDataStore",
86    "UpdateTrail",
87];
88
89pub struct CloudTrailService {
90    state: SharedCloudTrailState,
91    snapshot_store: Option<Arc<dyn SnapshotStore>>,
92    snapshot_lock: Arc<AsyncMutex<()>>,
93}
94
95impl CloudTrailService {
96    pub fn new(state: SharedCloudTrailState) -> Self {
97        Self {
98            state,
99            snapshot_store: None,
100            snapshot_lock: Arc::new(AsyncMutex::new(())),
101        }
102    }
103
104    pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
105        self.snapshot_store = Some(store);
106        self
107    }
108
109    async fn save(&self) {
110        save_snapshot(
111            &self.state,
112            self.snapshot_store.clone(),
113            &self.snapshot_lock,
114        )
115        .await;
116    }
117}
118
119#[async_trait]
120impl AwsService for CloudTrailService {
121    fn service_name(&self) -> &str {
122        "cloudtrail"
123    }
124
125    async fn handle(&self, request: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
126        let mutates = is_mutating(request.action.as_str());
127        let result = dispatch(self, &request);
128        if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
129            self.save().await;
130        }
131        result
132    }
133
134    fn supported_actions(&self) -> &[&str] {
135        CLOUDTRAIL_ACTIONS
136    }
137}
138
139/// Read-only operations do not mutate state and skip the persistence save.
140fn is_mutating(action: &str) -> bool {
141    !(action.starts_with("Describe")
142        || action.starts_with("List")
143        || action.starts_with("Get")
144        || action == "LookupEvents"
145        || action == "GenerateQuery")
146}
147
148fn dispatch(s: &CloudTrailService, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
149    let b = parse(req)?;
150    validate_input(req.action.as_str(), &b)?;
151    let ctx = Ctx {
152        account: req.account_id.clone(),
153        region: req.region.clone(),
154    };
155    match req.action.as_str() {
156        // trails
157        "CreateTrail" => s.create_trail(&ctx, &b),
158        "GetTrail" => s.get_trail(&ctx, &b),
159        "UpdateTrail" => s.update_trail(&ctx, &b),
160        "DeleteTrail" => s.delete_trail(&ctx, &b),
161        "DescribeTrails" => s.describe_trails(&ctx, &b),
162        "ListTrails" => s.list_trails(&ctx, &b),
163        "GetTrailStatus" => s.get_trail_status(&ctx, &b),
164        "StartLogging" => s.set_logging(&ctx, &b, true),
165        "StopLogging" => s.set_logging(&ctx, &b, false),
166        // event selectors
167        "GetEventSelectors" => s.get_event_selectors(&ctx, &b),
168        "PutEventSelectors" => s.put_event_selectors(&ctx, &b),
169        "GetInsightSelectors" => s.get_insight_selectors(&ctx, &b),
170        "PutInsightSelectors" => s.put_insight_selectors(&ctx, &b),
171        // event data stores
172        "CreateEventDataStore" => s.create_event_data_store(&ctx, &b),
173        "GetEventDataStore" => s.get_event_data_store(&ctx, &b),
174        "UpdateEventDataStore" => s.update_event_data_store(&ctx, &b),
175        "DeleteEventDataStore" => s.delete_event_data_store(&ctx, &b),
176        "ListEventDataStores" => s.list_event_data_stores(&ctx, &b),
177        "RestoreEventDataStore" => s.restore_event_data_store(&ctx, &b),
178        "StartEventDataStoreIngestion" => s.set_ingestion(&ctx, &b, true),
179        "StopEventDataStoreIngestion" => s.set_ingestion(&ctx, &b, false),
180        "EnableFederation" => s.enable_federation(&ctx, &b),
181        "DisableFederation" => s.disable_federation(&ctx, &b),
182        // channels
183        "CreateChannel" => s.create_channel(&ctx, &b),
184        "GetChannel" => s.get_channel(&ctx, &b),
185        "UpdateChannel" => s.update_channel(&ctx, &b),
186        "DeleteChannel" => s.delete_channel(&ctx, &b),
187        "ListChannels" => s.list_channels(&ctx, &b),
188        // imports
189        "StartImport" => s.start_import(&ctx, &b),
190        "StopImport" => s.stop_import(&ctx, &b),
191        "GetImport" => s.get_import(&ctx, &b),
192        "ListImports" => s.list_imports(&ctx, &b),
193        "ListImportFailures" => s.list_import_failures(&ctx, &b),
194        // queries
195        "StartQuery" => s.start_query(&ctx, &b),
196        "DescribeQuery" => s.describe_query(&ctx, &b),
197        "GetQueryResults" => s.get_query_results(&ctx, &b),
198        "CancelQuery" => s.cancel_query(&ctx, &b),
199        "ListQueries" => s.list_queries(&ctx, &b),
200        "GenerateQuery" => s.generate_query(&ctx, &b),
201        // dashboards
202        "CreateDashboard" => s.create_dashboard(&ctx, &b),
203        "GetDashboard" => s.get_dashboard(&ctx, &b),
204        "UpdateDashboard" => s.update_dashboard(&ctx, &b),
205        "DeleteDashboard" => s.delete_dashboard(&ctx, &b),
206        "ListDashboards" => s.list_dashboards(&ctx, &b),
207        "StartDashboardRefresh" => s.start_dashboard_refresh(&ctx, &b),
208        // resource policy
209        "PutResourcePolicy" => s.put_resource_policy(&ctx, &b),
210        "GetResourcePolicy" => s.get_resource_policy(&ctx, &b),
211        "DeleteResourcePolicy" => s.delete_resource_policy(&ctx, &b),
212        // organization delegated admin
213        "RegisterOrganizationDelegatedAdmin" => s.register_delegated_admin(&ctx, &b),
214        "DeregisterOrganizationDelegatedAdmin" => s.deregister_delegated_admin(&ctx, &b),
215        // event configuration
216        "GetEventConfiguration" => s.get_event_configuration(&ctx, &b),
217        "PutEventConfiguration" => s.put_event_configuration(&ctx, &b),
218        // tagging
219        "AddTags" => s.add_tags(&ctx, &b),
220        "RemoveTags" => s.remove_tags(&ctx, &b),
221        "ListTags" => s.list_tags(&ctx, &b),
222        // read-only / empty result ops
223        "LookupEvents" => lookup_events(&b),
224        "ListPublicKeys" => list_public_keys(),
225        "ListInsightsData" => list_insights_data(&b),
226        "ListInsightsMetricData" => list_insights_metric_data(&b),
227        "SearchSampleQueries" => search_sample_queries(&b),
228        _ => Err(AwsServiceError::action_not_implemented(
229            s.service_name(),
230            &req.action,
231        )),
232    }
233}
234
235// ===================== helpers =====================
236
237/// Per-request account + region context.
238struct Ctx {
239    account: String,
240    region: String,
241}
242
243fn ok(v: Value) -> Result<AwsResponse, AwsServiceError> {
244    Ok(AwsResponse::json_value(StatusCode::OK, v))
245}
246
247fn empty_ok() -> Result<AwsResponse, AwsServiceError> {
248    ok(json!({}))
249}
250
251fn parse(req: &AwsRequest) -> Result<Value, AwsServiceError> {
252    if req.body.is_empty() {
253        return Ok(json!({}));
254    }
255    serde_json::from_slice(&req.body)
256        .map_err(|e| invalid_param(&format!("Request body is malformed: {e}")))
257}
258
259fn err(code: &str, msg: &str) -> AwsServiceError {
260    AwsServiceError::aws_error(StatusCode::BAD_REQUEST, code, msg)
261}
262
263fn invalid_param(msg: &str) -> AwsServiceError {
264    err("InvalidParameterException", msg)
265}
266
267fn trail_not_found(name: &str) -> AwsServiceError {
268    err(
269        "TrailNotFoundException",
270        &format!("Unknown trail: {name} for the given account."),
271    )
272}
273
274fn eds_not_found(arn: &str) -> AwsServiceError {
275    err(
276        "EventDataStoreNotFoundException",
277        &format!("The specified event data store {arn} was not found."),
278    )
279}
280
281fn channel_not_found(arn: &str) -> AwsServiceError {
282    err(
283        "ChannelNotFoundException",
284        &format!("The specified channel {arn} was not found."),
285    )
286}
287
288fn import_not_found(id: &str) -> AwsServiceError {
289    err(
290        "ImportNotFoundException",
291        &format!("The specified import {id} was not found."),
292    )
293}
294
295fn query_not_found(id: &str) -> AwsServiceError {
296    err(
297        "QueryIdNotFoundException",
298        &format!("The specified query ID {id} was not found."),
299    )
300}
301
302fn dashboard_not_found(id: &str) -> AwsServiceError {
303    err(
304        "ResourceNotFoundException",
305        &format!("The specified dashboard {id} was not found."),
306    )
307}
308
309/// Read a required, non-empty string field.
310fn req_str<'a>(b: &'a Value, f: &str) -> Result<&'a str, AwsServiceError> {
311    b.get(f)
312        .and_then(Value::as_str)
313        .filter(|s| !s.is_empty())
314        .ok_or_else(|| invalid_param(&format!("{f} is required.")))
315}
316
317fn opt_str<'a>(b: &'a Value, f: &str) -> Option<&'a str> {
318    b.get(f).and_then(Value::as_str)
319}
320
321fn now_ts() -> f64 {
322    chrono::Utc::now().timestamp() as f64
323}
324
325/// Current time as an ISO-8601 / RFC-3339 UTC string (e.g. `2023-01-01T00:00:00Z`),
326/// matching the model's string type for the `TimeLogging{Started,Stopped}` members.
327fn now_iso() -> String {
328    chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
329}
330
331fn new_uuid() -> String {
332    Uuid::new_v4().to_string()
333}
334
335/// Trail ARNs and bare names are accepted interchangeably by the trail ops;
336/// normalise both to the bare trail name used as the state key.
337fn trail_key(name: &str) -> &str {
338    match name.rsplit_once(":trail/") {
339        Some((_, n)) => n,
340        None => name,
341    }
342}
343
344fn trail_arn(ctx: &Ctx, name: &str) -> String {
345    format!(
346        "arn:aws:cloudtrail:{}:{}:trail/{}",
347        ctx.region, ctx.account, name
348    )
349}
350
351/// Convert a maybe-present `Marker`/`NextToken` window over ordered rows into a
352/// page + optional next token. The token is the opaque start index.
353fn paginate(rows: Vec<Value>, b: &Value, default_max: usize) -> (Vec<Value>, Option<String>) {
354    let start = b
355        .get("NextToken")
356        .and_then(Value::as_str)
357        .and_then(|t| t.parse::<usize>().ok())
358        .unwrap_or(0);
359    let max = b
360        .get("MaxResults")
361        .and_then(Value::as_u64)
362        .map(|m| m.clamp(1, 10_000) as usize)
363        .unwrap_or(default_max);
364    let end = start.saturating_add(max).min(rows.len());
365    let page = rows.get(start..end).unwrap_or(&[]).to_vec();
366    // Zero-pad to >=4 chars so the token satisfies the model's
367    // `PaginationToken @length(min:4)` validation on the follow-up request.
368    // The read side parses with `parse::<usize>()`, which recovers the value
369    // from a padded token (e.g. "0100" -> 100).
370    let next = if end < rows.len() {
371        Some(format!("{end:04}"))
372    } else {
373        None
374    };
375    (page, next)
376}
377
378fn list_response(key: &str, rows: Vec<Value>, b: &Value) -> Result<AwsResponse, AwsServiceError> {
379    let (page, next) = paginate(rows, b, 100);
380    let mut out = Map::new();
381    out.insert(key.to_string(), Value::Array(page));
382    if let Some(t) = next {
383        out.insert("NextToken".to_string(), json!(t));
384    }
385    ok(Value::Object(out))
386}
387
388/// Copy a set of fields verbatim from `src` into `dst` when present.
389fn copy_fields(src: &Value, dst: &mut Map<String, Value>, fields: &[&str]) {
390    for f in fields {
391        if let Some(v) = src.get(*f) {
392            dst.insert((*f).to_string(), v.clone());
393        }
394    }
395}
396
397/// Store the request's `Tags`/`TagsList` into the account tag map under `key`.
398fn store_tags(data: &mut CloudTrailData, key: &str, b: &Value) {
399    let tags = b
400        .get("TagsList")
401        .or_else(|| b.get("Tags"))
402        .and_then(Value::as_array);
403    if let Some(tags) = tags {
404        let entry = data.tags.entry(key.to_string()).or_default();
405        for t in tags {
406            if let Some(k) = t.get("Key").and_then(Value::as_str) {
407                let v = t.get("Value").and_then(Value::as_str).unwrap_or("");
408                entry.insert(k.to_string(), v.to_string());
409            }
410        }
411    }
412}
413
414// ===================== trails =====================
415
416/// The trail describe-object fields carried by CreateTrail/UpdateTrail inputs.
417const TRAIL_INPUT_FIELDS: &[&str] = &[
418    "S3BucketName",
419    "S3KeyPrefix",
420    "IncludeGlobalServiceEvents",
421    "IsMultiRegionTrail",
422    "CloudWatchLogsLogGroupArn",
423    "CloudWatchLogsRoleArn",
424    "KmsKeyId",
425    "IsOrganizationTrail",
426];
427
428impl CloudTrailService {
429    fn build_trail(&self, ctx: &Ctx, name: &str, b: &Value, existing: Option<&Value>) -> Value {
430        let mut t = Map::new();
431        if let Some(prev) = existing {
432            if let Some(obj) = prev.as_object() {
433                t = obj.clone();
434            }
435        }
436        t.insert("Name".into(), json!(name));
437        t.insert("TrailARN".into(), json!(trail_arn(ctx, name)));
438        t.insert("HomeRegion".into(), json!(ctx.region));
439        copy_fields(b, &mut t, TRAIL_INPUT_FIELDS);
440        // Defaults for booleans not previously set.
441        t.entry("IncludeGlobalServiceEvents").or_insert(json!(true));
442        t.entry("IsMultiRegionTrail").or_insert(json!(false));
443        t.entry("IsOrganizationTrail").or_insert(json!(false));
444        // SNS topic name -> ARN.
445        if let Some(sns) = opt_str(b, "SnsTopicName") {
446            t.insert("SnsTopicName".into(), json!(sns));
447            t.insert(
448                "SnsTopicARN".into(),
449                json!(format!(
450                    "arn:aws:sns:{}:{}:{}",
451                    ctx.region, ctx.account, sns
452                )),
453            );
454        }
455        let validation = b
456            .get("EnableLogFileValidation")
457            .and_then(Value::as_bool)
458            .unwrap_or_else(|| {
459                existing
460                    .and_then(|e| e.get("LogFileValidationEnabled"))
461                    .and_then(Value::as_bool)
462                    .unwrap_or(false)
463            });
464        t.insert("LogFileValidationEnabled".into(), json!(validation));
465        t.entry("HasCustomEventSelectors").or_insert(json!(false));
466        t.entry("HasInsightSelectors").or_insert(json!(false));
467        Value::Object(t)
468    }
469
470    /// Build the CreateTrail/UpdateTrail output (a subset of the full Trail).
471    fn trail_mutation_output(trail: &Value) -> Value {
472        let mut out = Map::new();
473        copy_fields(
474            trail,
475            &mut out,
476            &[
477                "Name",
478                "S3BucketName",
479                "S3KeyPrefix",
480                "SnsTopicName",
481                "SnsTopicARN",
482                "IncludeGlobalServiceEvents",
483                "IsMultiRegionTrail",
484                "TrailARN",
485                "LogFileValidationEnabled",
486                "CloudWatchLogsLogGroupArn",
487                "CloudWatchLogsRoleArn",
488                "KmsKeyId",
489                "IsOrganizationTrail",
490            ],
491        );
492        Value::Object(out)
493    }
494
495    fn create_trail(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
496        let name = req_str(b, "Name")?.to_string();
497        req_str(b, "S3BucketName")?;
498        let mut guard = self.state.write();
499        let data = guard.get_or_create(&ctx.account);
500        if data.trails.contains_key(&name) {
501            return Err(err(
502                "TrailAlreadyExistsException",
503                &format!("Trail {name} already exists."),
504            ));
505        }
506        let trail = self.build_trail(ctx, &name, b, None);
507        let arn = trail_arn(ctx, &name);
508        store_tags(data, &arn, b);
509        data.trails.insert(name.clone(), trail.clone());
510        data.trail_logging.insert(name, false);
511        ok(Self::trail_mutation_output(&trail))
512    }
513
514    fn get_trail(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
515        let name = trail_key(req_str(b, "Name")?).to_string();
516        let guard = self.state.read();
517        let data = guard.get(&ctx.account);
518        match data.and_then(|d| d.trails.get(&name)) {
519            Some(t) => ok(json!({ "Trail": t })),
520            None => Err(trail_not_found(&name)),
521        }
522    }
523
524    fn update_trail(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
525        let name = trail_key(req_str(b, "Name")?).to_string();
526        let mut guard = self.state.write();
527        let data = guard.get_or_create(&ctx.account);
528        let existing = data
529            .trails
530            .get(&name)
531            .cloned()
532            .ok_or_else(|| trail_not_found(&name))?;
533        let trail = self.build_trail(ctx, &name, b, Some(&existing));
534        data.trails.insert(name, trail.clone());
535        ok(Self::trail_mutation_output(&trail))
536    }
537
538    fn delete_trail(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
539        let name = trail_key(req_str(b, "Name")?).to_string();
540        let mut guard = self.state.write();
541        let data = guard.get_or_create(&ctx.account);
542        if data.trails.remove(&name).is_none() {
543            return Err(trail_not_found(&name));
544        }
545        data.trail_logging.remove(&name);
546        data.trail_status.remove(&name);
547        data.event_selectors.remove(&name);
548        data.insight_selectors.remove(&name);
549        // Trail ARN is name-derived, so a destroy+recreate reuses it. Purge
550        // tags and event configuration keyed by the ARN to avoid leaking stale
551        // state onto the recreated trail.
552        let arn = trail_arn(ctx, &name);
553        data.tags.remove(&arn);
554        data.event_configurations.remove(&arn);
555        empty_ok()
556    }
557
558    fn describe_trails(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
559        let guard = self.state.read();
560        let data = guard.get(&ctx.account);
561        let wanted: Option<Vec<String>> =
562            b.get("trailNameList").and_then(Value::as_array).map(|a| {
563                a.iter()
564                    .filter_map(|v| v.as_str())
565                    .map(|s| trail_key(s).to_string())
566                    .collect()
567            });
568        let mut list = Vec::new();
569        if let Some(data) = data {
570            match &wanted {
571                Some(names) => {
572                    for n in names {
573                        if let Some(t) = data.trails.get(n) {
574                            list.push(t.clone());
575                        }
576                    }
577                }
578                None => list.extend(data.trails.values().cloned()),
579            }
580        }
581        ok(json!({ "trailList": list }))
582    }
583
584    fn list_trails(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
585        let guard = self.state.read();
586        let rows: Vec<Value> = guard
587            .get(&ctx.account)
588            .map(|d| {
589                d.trails
590                    .values()
591                    .map(|t| {
592                        json!({
593                            "TrailARN": t.get("TrailARN").cloned().unwrap_or(Value::Null),
594                            "Name": t.get("Name").cloned().unwrap_or(Value::Null),
595                            "HomeRegion": t.get("HomeRegion").cloned().unwrap_or(Value::Null),
596                        })
597                    })
598                    .collect()
599            })
600            .unwrap_or_default();
601        list_response("Trails", rows, b)
602    }
603
604    fn get_trail_status(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
605        let name = trail_key(req_str(b, "Name")?).to_string();
606        let guard = self.state.read();
607        let data = guard.get(&ctx.account);
608        let Some(data) = data else {
609            return Err(trail_not_found(&name));
610        };
611        if !data.trails.contains_key(&name) {
612            return Err(trail_not_found(&name));
613        }
614        let is_logging = data.trail_logging.get(&name).copied().unwrap_or(false);
615        let mut out = Map::new();
616        out.insert("IsLogging".into(), json!(is_logging));
617        if let Some(status) = data.trail_status.get(&name) {
618            if let Some(obj) = status.as_object() {
619                for (k, v) in obj {
620                    out.insert(k.clone(), v.clone());
621                }
622            }
623        }
624        ok(Value::Object(out))
625    }
626
627    fn set_logging(
628        &self,
629        ctx: &Ctx,
630        b: &Value,
631        logging: bool,
632    ) -> Result<AwsResponse, AwsServiceError> {
633        let name = trail_key(req_str(b, "Name")?).to_string();
634        let mut guard = self.state.write();
635        let data = guard.get_or_create(&ctx.account);
636        if !data.trails.contains_key(&name) {
637            return Err(trail_not_found(&name));
638        }
639        data.trail_logging.insert(name.clone(), logging);
640        let status = data.trail_status.entry(name).or_insert_with(|| json!({}));
641        if let Some(obj) = status.as_object_mut() {
642            if logging {
643                obj.insert("StartLoggingTime".into(), json!(now_ts()));
644                obj.insert("TimeLoggingStarted".into(), json!(now_iso()));
645            } else {
646                obj.insert("StopLoggingTime".into(), json!(now_ts()));
647                obj.insert("TimeLoggingStopped".into(), json!(now_iso()));
648            }
649        }
650        empty_ok()
651    }
652}
653
654// ===================== event selectors =====================
655
656impl CloudTrailService {
657    fn get_event_selectors(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
658        let name = trail_key(req_str(b, "TrailName")?).to_string();
659        let guard = self.state.read();
660        let data = guard.get(&ctx.account);
661        let Some(data) = data.filter(|d| d.trails.contains_key(&name)) else {
662            return Err(trail_not_found(&name));
663        };
664        let arn = trail_arn(ctx, &name);
665        match data.event_selectors.get(&name) {
666            Some(sel) => {
667                let mut out = Map::new();
668                out.insert("TrailARN".into(), json!(arn));
669                if let Some(obj) = sel.as_object() {
670                    for (k, v) in obj {
671                        out.insert(k.clone(), v.clone());
672                    }
673                }
674                ok(Value::Object(out))
675            }
676            None => ok(json!({
677                "TrailARN": arn,
678                "EventSelectors": [{
679                    "ReadWriteType": "All",
680                    "IncludeManagementEvents": true,
681                    "DataResources": [],
682                    "ExcludeManagementEventSources": [],
683                }],
684            })),
685        }
686    }
687
688    fn put_event_selectors(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
689        let name = trail_key(req_str(b, "TrailName")?).to_string();
690        let mut guard = self.state.write();
691        let data = guard.get_or_create(&ctx.account);
692        if !data.trails.contains_key(&name) {
693            return Err(trail_not_found(&name));
694        }
695        let mut sel = Map::new();
696        let mut out = Map::new();
697        out.insert("TrailARN".into(), json!(trail_arn(ctx, &name)));
698        if let Some(es) = b.get("EventSelectors") {
699            sel.insert("EventSelectors".into(), es.clone());
700            out.insert("EventSelectors".into(), es.clone());
701        }
702        if let Some(aes) = b.get("AdvancedEventSelectors") {
703            sel.insert("AdvancedEventSelectors".into(), aes.clone());
704            out.insert("AdvancedEventSelectors".into(), aes.clone());
705        }
706        data.event_selectors
707            .insert(name.clone(), Value::Object(sel));
708        if let Some(t) = data.trails.get_mut(&name) {
709            if let Some(obj) = t.as_object_mut() {
710                obj.insert("HasCustomEventSelectors".into(), json!(true));
711            }
712        }
713        ok(Value::Object(out))
714    }
715
716    fn get_insight_selectors(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
717        // Insight selectors can be keyed on a trail or an event data store.
718        // `GetInsightSelectors` does not declare `EventDataStoreNotFoundException`;
719        // a store without configured insights returns `InsightNotEnabledException`.
720        if let Some(eds) = opt_str(b, "EventDataStore").filter(|s| !s.is_empty()) {
721            let guard = self.state.read();
722            let sel = guard
723                .get(&ctx.account)
724                .and_then(|d| d.insight_selectors.get(eds))
725                .cloned()
726                .ok_or_else(|| {
727                    err(
728                        "InsightNotEnabledException",
729                        "Insights are not enabled on the given event data store.",
730                    )
731                })?;
732            let mut out = sel.as_object().cloned().unwrap_or_default();
733            out.insert("EventDataStoreArn".into(), json!(eds));
734            return ok(Value::Object(out));
735        }
736        let name = trail_key(req_str(b, "TrailName")?).to_string();
737        let guard = self.state.read();
738        let data = guard.get(&ctx.account);
739        let Some(data) = data.filter(|d| d.trails.contains_key(&name)) else {
740            return Err(trail_not_found(&name));
741        };
742        let sel = data.insight_selectors.get(&name).cloned().ok_or_else(|| {
743            err(
744                "InsightNotEnabledException",
745                "Insights are not enabled on the given trail.",
746            )
747        })?;
748        let mut out = sel.as_object().cloned().unwrap_or_default();
749        out.insert("TrailARN".into(), json!(trail_arn(ctx, &name)));
750        ok(Value::Object(out))
751    }
752
753    fn put_insight_selectors(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
754        let insight = b
755            .get("InsightSelectors")
756            .cloned()
757            .ok_or_else(|| invalid_param("InsightSelectors is required."))?;
758        // `PutInsightSelectors` does not declare `EventDataStoreNotFoundException`;
759        // the destination store is validated lazily, so persist against the
760        // supplied ARN directly.
761        if let Some(eds) = opt_str(b, "EventDataStore").filter(|s| !s.is_empty()) {
762            let mut guard = self.state.write();
763            let data = guard.get_or_create(&ctx.account);
764            let stored = json!({ "InsightSelectors": insight });
765            data.insight_selectors.insert(eds.to_string(), stored);
766            let mut out = Map::new();
767            out.insert("EventDataStoreArn".into(), json!(eds));
768            out.insert("InsightSelectors".into(), b["InsightSelectors"].clone());
769            if let Some(dest) = opt_str(b, "InsightsDestination") {
770                out.insert("InsightsDestination".into(), json!(dest));
771            }
772            return ok(Value::Object(out));
773        }
774        let name = trail_key(
775            opt_str(b, "TrailName")
776                .filter(|s| !s.is_empty())
777                .ok_or_else(|| {
778                    err(
779                        "InvalidParameterCombinationException",
780                        "Either TrailName or EventDataStore must be specified.",
781                    )
782                })?,
783        )
784        .to_string();
785        let mut guard = self.state.write();
786        let data = guard.get_or_create(&ctx.account);
787        if !data.trails.contains_key(&name) {
788            return Err(trail_not_found(&name));
789        }
790        let stored = json!({ "InsightSelectors": insight });
791        data.insight_selectors.insert(name.clone(), stored);
792        if let Some(t) = data.trails.get_mut(&name) {
793            if let Some(obj) = t.as_object_mut() {
794                obj.insert("HasInsightSelectors".into(), json!(true));
795            }
796        }
797        ok(json!({
798            "TrailARN": trail_arn(ctx, &name),
799            "InsightSelectors": b["InsightSelectors"],
800        }))
801    }
802}
803
804// ===================== event data stores =====================
805
806const EDS_OUTPUT_FIELDS: &[&str] = &[
807    "EventDataStoreArn",
808    "Name",
809    "Status",
810    "AdvancedEventSelectors",
811    "MultiRegionEnabled",
812    "OrganizationEnabled",
813    "RetentionPeriod",
814    "TerminationProtectionEnabled",
815    "CreatedTimestamp",
816    "UpdatedTimestamp",
817    "KmsKeyId",
818    "BillingMode",
819    "FederationStatus",
820    "FederationRoleArn",
821];
822
823impl CloudTrailService {
824    fn eds_output(eds: &Value) -> Value {
825        let mut out = Map::new();
826        copy_fields(eds, &mut out, EDS_OUTPUT_FIELDS);
827        Value::Object(out)
828    }
829
830    /// The `EventDataStore` summary shape carried by `ListEventDataStores`
831    /// (a subset of the full describe shape — no billing/KMS/federation).
832    fn eds_summary(eds: &Value) -> Value {
833        let mut out = Map::new();
834        copy_fields(
835            eds,
836            &mut out,
837            &[
838                "EventDataStoreArn",
839                "Name",
840                "TerminationProtectionEnabled",
841                "Status",
842                "AdvancedEventSelectors",
843                "MultiRegionEnabled",
844                "OrganizationEnabled",
845                "RetentionPeriod",
846                "CreatedTimestamp",
847                "UpdatedTimestamp",
848            ],
849        );
850        Value::Object(out)
851    }
852
853    fn create_event_data_store(
854        &self,
855        ctx: &Ctx,
856        b: &Value,
857    ) -> Result<AwsResponse, AwsServiceError> {
858        let name = req_str(b, "Name")?.to_string();
859        let mut guard = self.state.write();
860        let data = guard.get_or_create(&ctx.account);
861        if data
862            .event_data_stores
863            .values()
864            .any(|e| e.get("Name").and_then(Value::as_str) == Some(&name))
865        {
866            return Err(err(
867                "EventDataStoreAlreadyExistsException",
868                &format!("An event data store already exists with the name {name}."),
869            ));
870        }
871        let arn = format!(
872            "arn:aws:cloudtrail:{}:{}:eventdatastore/{}",
873            ctx.region,
874            ctx.account,
875            new_uuid()
876        );
877        let mut eds = Map::new();
878        eds.insert("EventDataStoreArn".into(), json!(arn));
879        eds.insert("Name".into(), json!(name));
880        eds.insert("Status".into(), json!("ENABLED"));
881        // AWS defaults an event data store with no explicit selectors to a
882        // single "Default management events" advanced selector rather than an
883        // empty list.
884        eds.insert(
885            "AdvancedEventSelectors".into(),
886            b.get("AdvancedEventSelectors").cloned().unwrap_or_else(|| {
887                json!([{
888                    "Name": "Default management events",
889                    "FieldSelectors": [{
890                        "Field": "eventCategory",
891                        "Equals": ["Management"]
892                    }]
893                }])
894            }),
895        );
896        eds.insert(
897            "MultiRegionEnabled".into(),
898            b.get("MultiRegionEnabled").cloned().unwrap_or(json!(true)),
899        );
900        eds.insert(
901            "OrganizationEnabled".into(),
902            b.get("OrganizationEnabled")
903                .cloned()
904                .unwrap_or(json!(false)),
905        );
906        eds.insert(
907            "RetentionPeriod".into(),
908            b.get("RetentionPeriod").cloned().unwrap_or(json!(2555)),
909        );
910        eds.insert(
911            "TerminationProtectionEnabled".into(),
912            b.get("TerminationProtectionEnabled")
913                .cloned()
914                .unwrap_or(json!(true)),
915        );
916        eds.insert(
917            "BillingMode".into(),
918            b.get("BillingMode")
919                .cloned()
920                .unwrap_or(json!("EXTENDABLE_RETENTION_PRICING")),
921        );
922        eds.insert("CreatedTimestamp".into(), json!(now_ts()));
923        eds.insert("UpdatedTimestamp".into(), json!(now_ts()));
924        copy_fields(b, &mut eds, &["KmsKeyId"]);
925        if let Some(tags) = b.get("TagsList") {
926            eds.insert("TagsList".into(), tags.clone());
927        }
928        let eds = Value::Object(eds);
929        store_tags(data, &arn, b);
930        data.event_data_stores.insert(arn, eds.clone());
931        ok(eds)
932    }
933
934    fn get_event_data_store(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
935        let arn = req_str(b, "EventDataStore")?.to_string();
936        let guard = self.state.read();
937        match guard
938            .get(&ctx.account)
939            .and_then(|d| d.event_data_stores.get(&arn))
940        {
941            Some(eds) => ok(Self::eds_output(eds)),
942            None => Err(eds_not_found(&arn)),
943        }
944    }
945
946    fn update_event_data_store(
947        &self,
948        ctx: &Ctx,
949        b: &Value,
950    ) -> Result<AwsResponse, AwsServiceError> {
951        let arn = req_str(b, "EventDataStore")?.to_string();
952        let mut guard = self.state.write();
953        let data = guard.get_or_create(&ctx.account);
954        let eds = data
955            .event_data_stores
956            .get_mut(&arn)
957            .ok_or_else(|| eds_not_found(&arn))?;
958        if let Some(obj) = eds.as_object_mut() {
959            copy_fields(
960                b,
961                obj,
962                &[
963                    "Name",
964                    "AdvancedEventSelectors",
965                    "MultiRegionEnabled",
966                    "OrganizationEnabled",
967                    "RetentionPeriod",
968                    "TerminationProtectionEnabled",
969                    "KmsKeyId",
970                    "BillingMode",
971                ],
972            );
973            obj.insert("UpdatedTimestamp".into(), json!(now_ts()));
974        }
975        let eds = eds.clone();
976        ok(Self::eds_output(&eds))
977    }
978
979    fn delete_event_data_store(
980        &self,
981        ctx: &Ctx,
982        b: &Value,
983    ) -> Result<AwsResponse, AwsServiceError> {
984        let arn = req_str(b, "EventDataStore")?.to_string();
985        let mut guard = self.state.write();
986        let data = guard.get_or_create(&ctx.account);
987        let eds = data
988            .event_data_stores
989            .get_mut(&arn)
990            .ok_or_else(|| eds_not_found(&arn))?;
991        if eds
992            .get("TerminationProtectionEnabled")
993            .and_then(Value::as_bool)
994            == Some(true)
995        {
996            return Err(err(
997                "EventDataStoreTerminationProtectedException",
998                "The event data store cannot be deleted because termination protection is enabled.",
999            ));
1000        }
1001        // AWS keeps the store in PENDING_DELETION for a restore window.
1002        if let Some(obj) = eds.as_object_mut() {
1003            obj.insert("Status".into(), json!("PENDING_DELETION"));
1004            obj.insert("UpdatedTimestamp".into(), json!(now_ts()));
1005        }
1006        empty_ok()
1007    }
1008
1009    fn list_event_data_stores(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1010        let guard = self.state.read();
1011        let rows: Vec<Value> = guard
1012            .get(&ctx.account)
1013            .map(|d| {
1014                d.event_data_stores
1015                    .values()
1016                    .map(Self::eds_summary)
1017                    .collect()
1018            })
1019            .unwrap_or_default();
1020        list_response("EventDataStores", rows, b)
1021    }
1022
1023    fn restore_event_data_store(
1024        &self,
1025        ctx: &Ctx,
1026        b: &Value,
1027    ) -> Result<AwsResponse, AwsServiceError> {
1028        let arn = req_str(b, "EventDataStore")?.to_string();
1029        let mut guard = self.state.write();
1030        let data = guard.get_or_create(&ctx.account);
1031        let eds = data
1032            .event_data_stores
1033            .get_mut(&arn)
1034            .ok_or_else(|| eds_not_found(&arn))?;
1035        if let Some(obj) = eds.as_object_mut() {
1036            obj.insert("Status".into(), json!("ENABLED"));
1037            obj.insert("UpdatedTimestamp".into(), json!(now_ts()));
1038        }
1039        let eds = eds.clone();
1040        ok(Self::eds_output(&eds))
1041    }
1042
1043    fn set_ingestion(
1044        &self,
1045        ctx: &Ctx,
1046        b: &Value,
1047        start: bool,
1048    ) -> Result<AwsResponse, AwsServiceError> {
1049        let arn = req_str(b, "EventDataStore")?.to_string();
1050        let mut guard = self.state.write();
1051        let data = guard.get_or_create(&ctx.account);
1052        let eds = data
1053            .event_data_stores
1054            .get_mut(&arn)
1055            .ok_or_else(|| eds_not_found(&arn))?;
1056        if let Some(obj) = eds.as_object_mut() {
1057            obj.insert(
1058                "Status".into(),
1059                json!(if start {
1060                    "ENABLED"
1061                } else {
1062                    "STOPPED_INGESTION"
1063                }),
1064            );
1065            obj.insert("UpdatedTimestamp".into(), json!(now_ts()));
1066        }
1067        empty_ok()
1068    }
1069
1070    fn enable_federation(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1071        let arn = req_str(b, "EventDataStore")?.to_string();
1072        let role = req_str(b, "FederationRoleArn")?.to_string();
1073        let mut guard = self.state.write();
1074        let data = guard.get_or_create(&ctx.account);
1075        let eds = data
1076            .event_data_stores
1077            .get_mut(&arn)
1078            .ok_or_else(|| eds_not_found(&arn))?;
1079        if let Some(obj) = eds.as_object_mut() {
1080            obj.insert("FederationStatus".into(), json!("ENABLED"));
1081            obj.insert("FederationRoleArn".into(), json!(role));
1082        }
1083        ok(json!({
1084            "EventDataStoreArn": arn,
1085            "FederationStatus": "ENABLED",
1086            "FederationRoleArn": role,
1087        }))
1088    }
1089
1090    fn disable_federation(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1091        let arn = req_str(b, "EventDataStore")?.to_string();
1092        let mut guard = self.state.write();
1093        let data = guard.get_or_create(&ctx.account);
1094        let eds = data
1095            .event_data_stores
1096            .get_mut(&arn)
1097            .ok_or_else(|| eds_not_found(&arn))?;
1098        if let Some(obj) = eds.as_object_mut() {
1099            obj.insert("FederationStatus".into(), json!("DISABLED"));
1100        }
1101        ok(json!({
1102            "EventDataStoreArn": arn,
1103            "FederationStatus": "DISABLED",
1104        }))
1105    }
1106}
1107
1108// ===================== channels =====================
1109
1110impl CloudTrailService {
1111    fn create_channel(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1112        let name = req_str(b, "Name")?.to_string();
1113        let source = req_str(b, "Source")?.to_string();
1114        let destinations = b
1115            .get("Destinations")
1116            .cloned()
1117            .ok_or_else(|| invalid_param("Destinations is required."))?;
1118        let mut guard = self.state.write();
1119        let data = guard.get_or_create(&ctx.account);
1120        if data
1121            .channels
1122            .values()
1123            .any(|c| c.get("Name").and_then(Value::as_str) == Some(&name))
1124        {
1125            return Err(err(
1126                "ChannelAlreadyExistsException",
1127                &format!("A channel already exists with the name {name}."),
1128            ));
1129        }
1130        let arn = format!(
1131            "arn:aws:cloudtrail:{}:{}:channel/{}",
1132            ctx.region,
1133            ctx.account,
1134            new_uuid()
1135        );
1136        // The channel's source configuration. CreateChannel exposes no input
1137        // members for `ApplyToAllRegions`/`AdvancedEventSelectors`, so persist a
1138        // per-channel config (honouring `SourceConfig` if a client supplies it)
1139        // that GetChannel later echoes back verbatim, rather than a shared literal.
1140        let source_config = b
1141            .get("SourceConfig")
1142            .cloned()
1143            .unwrap_or_else(|| json!({ "ApplyToAllRegions": false, "AdvancedEventSelectors": [] }));
1144        let mut chan = Map::new();
1145        chan.insert("ChannelArn".into(), json!(arn));
1146        chan.insert("Name".into(), json!(name));
1147        chan.insert("Source".into(), json!(source));
1148        chan.insert("Destinations".into(), destinations);
1149        chan.insert("SourceConfig".into(), source_config);
1150        chan.insert("IngestionStatus".into(), json!({}));
1151        if let Some(tags) = b.get("Tags") {
1152            chan.insert("Tags".into(), tags.clone());
1153        }
1154        store_tags(data, &arn, b);
1155        let chan = Value::Object(chan);
1156        data.channels.insert(arn, chan.clone());
1157        // CreateChannel output echoes name/source/destinations/tags only.
1158        let mut out = chan.as_object().cloned().unwrap_or_default();
1159        out.remove("SourceConfig");
1160        out.remove("IngestionStatus");
1161        ok(Value::Object(out))
1162    }
1163
1164    fn get_channel(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1165        let arn = req_str(b, "Channel")?.to_string();
1166        let guard = self.state.read();
1167        let chan = guard
1168            .get(&ctx.account)
1169            .and_then(|d| d.channels.get(&arn))
1170            .cloned()
1171            .ok_or_else(|| channel_not_found(&arn))?;
1172        let mut out = chan.as_object().cloned().unwrap_or_default();
1173        out.remove("Tags");
1174        // Echo the channel's own persisted config, defaulting for channels
1175        // stored before these fields were persisted.
1176        out.entry("SourceConfig")
1177            .or_insert_with(|| json!({ "ApplyToAllRegions": false, "AdvancedEventSelectors": [] }));
1178        out.entry("IngestionStatus").or_insert_with(|| json!({}));
1179        ok(Value::Object(out))
1180    }
1181
1182    fn update_channel(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1183        let arn = req_str(b, "Channel")?.to_string();
1184        let mut guard = self.state.write();
1185        let data = guard.get_or_create(&ctx.account);
1186        let chan = data
1187            .channels
1188            .get_mut(&arn)
1189            .ok_or_else(|| channel_not_found(&arn))?;
1190        if let Some(obj) = chan.as_object_mut() {
1191            if let Some(name) = b.get("Name") {
1192                obj.insert("Name".into(), name.clone());
1193            }
1194            if let Some(dests) = b.get("Destinations") {
1195                obj.insert("Destinations".into(), dests.clone());
1196            }
1197        }
1198        let chan = chan.clone();
1199        let mut out = chan.as_object().cloned().unwrap_or_default();
1200        out.remove("Tags");
1201        out.remove("SourceConfig");
1202        out.remove("IngestionStatus");
1203        ok(Value::Object(out))
1204    }
1205
1206    fn delete_channel(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1207        let arn = req_str(b, "Channel")?.to_string();
1208        let mut guard = self.state.write();
1209        let data = guard.get_or_create(&ctx.account);
1210        if data.channels.remove(&arn).is_none() {
1211            return Err(channel_not_found(&arn));
1212        }
1213        empty_ok()
1214    }
1215
1216    fn list_channels(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1217        let guard = self.state.read();
1218        let rows: Vec<Value> = guard
1219            .get(&ctx.account)
1220            .map(|d| {
1221                d.channels
1222                    .values()
1223                    .map(|c| {
1224                        json!({
1225                            "ChannelArn": c.get("ChannelArn").cloned().unwrap_or(Value::Null),
1226                            "Name": c.get("Name").cloned().unwrap_or(Value::Null),
1227                        })
1228                    })
1229                    .collect()
1230            })
1231            .unwrap_or_default();
1232        list_response("Channels", rows, b)
1233    }
1234}
1235
1236// ===================== imports =====================
1237
1238impl CloudTrailService {
1239    fn start_import(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1240        let mut guard = self.state.write();
1241        let data = guard.get_or_create(&ctx.account);
1242        // A supplied ImportId restarts an existing import; otherwise a new
1243        // import is created.
1244        if let Some(id) = opt_str(b, "ImportId").filter(|s| !s.is_empty()) {
1245            let imp = data
1246                .imports
1247                .get_mut(id)
1248                .ok_or_else(|| import_not_found(id))?;
1249            if let Some(obj) = imp.as_object_mut() {
1250                obj.insert("ImportStatus".into(), json!("IN_PROGRESS"));
1251                obj.insert("UpdatedTimestamp".into(), json!(now_ts()));
1252            }
1253            return ok(imp.clone());
1254        }
1255        let id = new_uuid();
1256        let mut imp = Map::new();
1257        imp.insert("ImportId".into(), json!(id));
1258        copy_fields(
1259            b,
1260            &mut imp,
1261            &[
1262                "Destinations",
1263                "ImportSource",
1264                "StartEventTime",
1265                "EndEventTime",
1266            ],
1267        );
1268        imp.insert("ImportStatus".into(), json!("INITIALIZING"));
1269        imp.insert("CreatedTimestamp".into(), json!(now_ts()));
1270        imp.insert("UpdatedTimestamp".into(), json!(now_ts()));
1271        let imp = Value::Object(imp);
1272        data.imports.insert(id, imp.clone());
1273        ok(imp)
1274    }
1275
1276    fn stop_import(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1277        let id = req_str(b, "ImportId")?.to_string();
1278        let mut guard = self.state.write();
1279        let data = guard.get_or_create(&ctx.account);
1280        let imp = data
1281            .imports
1282            .get_mut(&id)
1283            .ok_or_else(|| import_not_found(&id))?;
1284        if let Some(obj) = imp.as_object_mut() {
1285            obj.insert("ImportStatus".into(), json!("STOPPED"));
1286            obj.insert("UpdatedTimestamp".into(), json!(now_ts()));
1287            obj.entry("ImportStatistics").or_insert(json!({
1288                "FilesCompleted": 0,
1289                "EventsCompleted": 0,
1290                "FailedEntries": 0,
1291            }));
1292        }
1293        ok(imp.clone())
1294    }
1295
1296    fn get_import(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1297        let id = req_str(b, "ImportId")?.to_string();
1298        let guard = self.state.read();
1299        let mut imp = guard
1300            .get(&ctx.account)
1301            .and_then(|d| d.imports.get(&id))
1302            .cloned()
1303            .ok_or_else(|| import_not_found(&id))?;
1304        if let Some(obj) = imp.as_object_mut() {
1305            obj.entry("ImportStatistics").or_insert(json!({
1306                "FilesCompleted": 0,
1307                "EventsCompleted": 0,
1308                "FailedEntries": 0,
1309            }));
1310        }
1311        ok(imp)
1312    }
1313
1314    fn list_imports(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1315        let status_filter = opt_str(b, "ImportStatus").map(str::to_string);
1316        let guard = self.state.read();
1317        let rows: Vec<Value> = guard
1318            .get(&ctx.account)
1319            .map(|d| {
1320                d.imports
1321                    .values()
1322                    .filter(|imp| {
1323                        status_filter.as_deref().is_none_or(|want| {
1324                            imp.get("ImportStatus").and_then(Value::as_str) == Some(want)
1325                        })
1326                    })
1327                    .map(|imp| {
1328                        json!({
1329                            "ImportId": imp.get("ImportId").cloned().unwrap_or(Value::Null),
1330                            "ImportStatus": imp.get("ImportStatus").cloned().unwrap_or(Value::Null),
1331                            "Destinations": imp.get("Destinations").cloned().unwrap_or(json!([])),
1332                            "CreatedTimestamp": imp.get("CreatedTimestamp").cloned().unwrap_or(Value::Null),
1333                            "UpdatedTimestamp": imp.get("UpdatedTimestamp").cloned().unwrap_or(Value::Null),
1334                        })
1335                    })
1336                    .collect()
1337            })
1338            .unwrap_or_default();
1339        list_response("Imports", rows, b)
1340    }
1341
1342    fn list_import_failures(&self, _ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1343        // `ListImportFailures` does not declare `ImportNotFoundException`; an
1344        // unknown import simply yields an empty failure page.
1345        req_str(b, "ImportId")?;
1346        list_response("Failures", Vec::new(), b)
1347    }
1348}
1349
1350// ===================== queries (CloudTrail Lake) =====================
1351
1352impl CloudTrailService {
1353    fn start_query(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1354        if opt_str(b, "QueryStatement").is_none() && opt_str(b, "QueryAlias").is_none() {
1355            return Err(invalid_param(
1356                "Either QueryStatement or QueryAlias must be specified.",
1357            ));
1358        }
1359        let id = new_uuid();
1360        let mut guard = self.state.write();
1361        let data = guard.get_or_create(&ctx.account);
1362        let mut q = Map::new();
1363        q.insert("QueryId".into(), json!(id));
1364        q.insert(
1365            "QueryString".into(),
1366            b.get("QueryStatement").cloned().unwrap_or(json!("")),
1367        );
1368        q.insert("QueryStatus".into(), json!("FINISHED"));
1369        q.insert("CreationTime".into(), json!(now_ts()));
1370        copy_fields(b, &mut q, &["DeliveryS3Uri", "QueryAlias"]);
1371        data.queries.insert(id.clone(), Value::Object(q));
1372        let mut out = Map::new();
1373        out.insert("QueryId".into(), json!(id));
1374        if let Some(owner) = opt_str(b, "EventDataStoreOwnerAccountId") {
1375            out.insert("EventDataStoreOwnerAccountId".into(), json!(owner));
1376        }
1377        ok(Value::Object(out))
1378    }
1379
1380    fn find_query<'a>(&self, data: &'a CloudTrailData, b: &Value) -> Option<&'a Value> {
1381        if let Some(id) = opt_str(b, "QueryId") {
1382            return data.queries.get(id);
1383        }
1384        if let Some(alias) = opt_str(b, "QueryAlias") {
1385            return data
1386                .queries
1387                .values()
1388                .find(|q| q.get("QueryAlias").and_then(Value::as_str) == Some(alias));
1389        }
1390        None
1391    }
1392
1393    fn describe_query(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1394        if opt_str(b, "QueryId").is_none() && opt_str(b, "QueryAlias").is_none() {
1395            return Err(invalid_param("QueryId or QueryAlias must be specified."));
1396        }
1397        let guard = self.state.read();
1398        let data = guard.get(&ctx.account);
1399        let q = data
1400            .and_then(|d| self.find_query(d, b))
1401            .ok_or_else(|| query_not_found(opt_str(b, "QueryId").unwrap_or("")))?;
1402        let mut out = Map::new();
1403        out.insert(
1404            "QueryId".into(),
1405            q.get("QueryId").cloned().unwrap_or(Value::Null),
1406        );
1407        out.insert(
1408            "QueryString".into(),
1409            q.get("QueryString").cloned().unwrap_or(json!("")),
1410        );
1411        out.insert(
1412            "QueryStatus".into(),
1413            q.get("QueryStatus").cloned().unwrap_or(json!("FINISHED")),
1414        );
1415        out.insert(
1416            "QueryStatistics".into(),
1417            json!({
1418                "EventsMatched": 0,
1419                "EventsScanned": 0,
1420                "BytesScanned": 0,
1421                "ExecutionTimeInMillis": 0,
1422                "CreationTime": q.get("CreationTime").cloned().unwrap_or(Value::Null),
1423            }),
1424        );
1425        out.insert("DeliveryStatus".into(), json!("SUCCESS"));
1426        ok(Value::Object(out))
1427    }
1428
1429    fn get_query_results(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1430        let id = req_str(b, "QueryId")?.to_string();
1431        let guard = self.state.read();
1432        let status = guard
1433            .get(&ctx.account)
1434            .and_then(|d| d.queries.get(&id))
1435            .and_then(|q| q.get("QueryStatus").and_then(Value::as_str))
1436            .map(str::to_string)
1437            .ok_or_else(|| query_not_found(&id))?;
1438        ok(json!({
1439            "QueryStatus": status,
1440            "QueryStatistics": {
1441                "ResultsCount": 0,
1442                "TotalResultsCount": 0,
1443                "BytesScanned": 0,
1444            },
1445            "QueryResultRows": [],
1446        }))
1447    }
1448
1449    fn cancel_query(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1450        let id = req_str(b, "QueryId")?.to_string();
1451        let mut guard = self.state.write();
1452        let data = guard.get_or_create(&ctx.account);
1453        let q = data
1454            .queries
1455            .get_mut(&id)
1456            .ok_or_else(|| query_not_found(&id))?;
1457        if let Some(obj) = q.as_object_mut() {
1458            obj.insert("QueryStatus".into(), json!("CANCELLED"));
1459        }
1460        let mut out = Map::new();
1461        out.insert("QueryId".into(), json!(id));
1462        out.insert("QueryStatus".into(), json!("CANCELLED"));
1463        if let Some(owner) = opt_str(b, "EventDataStoreOwnerAccountId") {
1464            out.insert("EventDataStoreOwnerAccountId".into(), json!(owner));
1465        }
1466        ok(Value::Object(out))
1467    }
1468
1469    fn list_queries(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1470        let eds = req_str(b, "EventDataStore")?.to_string();
1471        let status_filter = opt_str(b, "QueryStatus").map(str::to_string);
1472        let guard = self.state.read();
1473        let data = guard.get(&ctx.account);
1474        if !data
1475            .map(|d| d.event_data_stores.contains_key(&eds))
1476            .unwrap_or(false)
1477        {
1478            return Err(eds_not_found(&eds));
1479        }
1480        let rows: Vec<Value> = data
1481            .map(|d| {
1482                d.queries
1483                    .values()
1484                    .filter(|q| {
1485                        status_filter.as_deref().is_none_or(|want| {
1486                            q.get("QueryStatus").and_then(Value::as_str) == Some(want)
1487                        })
1488                    })
1489                    .map(|q| {
1490                        json!({
1491                            "QueryId": q.get("QueryId").cloned().unwrap_or(Value::Null),
1492                            "QueryStatus": q.get("QueryStatus").cloned().unwrap_or(Value::Null),
1493                            "CreationTime": q.get("CreationTime").cloned().unwrap_or(Value::Null),
1494                        })
1495                    })
1496                    .collect()
1497            })
1498            .unwrap_or_default();
1499        list_response("Queries", rows, b)
1500    }
1501
1502    fn generate_query(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1503        let stores = b
1504            .get("EventDataStores")
1505            .and_then(Value::as_array)
1506            .filter(|a| !a.is_empty())
1507            .ok_or_else(|| invalid_param("EventDataStores is required."))?;
1508        req_str(b, "Prompt")?;
1509        let guard = self.state.read();
1510        let data = guard.get(&ctx.account);
1511        for s in stores {
1512            if let Some(arn) = s.as_str() {
1513                if !data
1514                    .map(|d| d.event_data_stores.contains_key(arn))
1515                    .unwrap_or(false)
1516                {
1517                    return Err(eds_not_found(arn));
1518                }
1519            }
1520        }
1521        ok(json!({
1522            "QueryStatement": "SELECT eventName FROM eds LIMIT 10",
1523            "QueryAlias": "generated-query",
1524        }))
1525    }
1526}
1527
1528// ===================== dashboards =====================
1529
1530impl CloudTrailService {
1531    fn create_dashboard(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1532        let name = req_str(b, "Name")?.to_string();
1533        let mut guard = self.state.write();
1534        let data = guard.get_or_create(&ctx.account);
1535        let arn = format!(
1536            "arn:aws:cloudtrail:{}:{}:dashboard/{}",
1537            ctx.region, ctx.account, name
1538        );
1539        if data.dashboards.contains_key(&arn) {
1540            return Err(err(
1541                "ConflictException",
1542                &format!("A dashboard already exists with the name {name}."),
1543            ));
1544        }
1545        let mut d = Map::new();
1546        d.insert("DashboardArn".into(), json!(arn));
1547        d.insert("Name".into(), json!(name));
1548        d.insert("Type".into(), json!("CUSTOM"));
1549        d.insert("Status".into(), json!("CREATED"));
1550        d.insert(
1551            "Widgets".into(),
1552            b.get("Widgets").cloned().unwrap_or(json!([])),
1553        );
1554        d.insert(
1555            "TerminationProtectionEnabled".into(),
1556            b.get("TerminationProtectionEnabled")
1557                .cloned()
1558                .unwrap_or(json!(false)),
1559        );
1560        if let Some(rs) = b.get("RefreshSchedule") {
1561            d.insert("RefreshSchedule".into(), rs.clone());
1562        }
1563        if let Some(tags) = b.get("TagsList") {
1564            d.insert("TagsList".into(), tags.clone());
1565        }
1566        d.insert("CreatedTimestamp".into(), json!(now_ts()));
1567        d.insert("UpdatedTimestamp".into(), json!(now_ts()));
1568        store_tags(data, &arn, b);
1569        let d = Value::Object(d);
1570        data.dashboards.insert(arn, d.clone());
1571        // CreateDashboard output.
1572        let mut out = d.as_object().cloned().unwrap_or_default();
1573        out.remove("Status");
1574        out.remove("CreatedTimestamp");
1575        out.remove("UpdatedTimestamp");
1576        ok(Value::Object(out))
1577    }
1578
1579    fn get_dashboard(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1580        let id = req_str(b, "DashboardId")?.to_string();
1581        let guard = self.state.read();
1582        let d = guard
1583            .get(&ctx.account)
1584            .and_then(|s| s.dashboards.get(&id))
1585            .cloned()
1586            .ok_or_else(|| dashboard_not_found(&id))?;
1587        let mut out = d.as_object().cloned().unwrap_or_default();
1588        out.remove("Name");
1589        out.remove("TagsList");
1590        ok(Value::Object(out))
1591    }
1592
1593    fn update_dashboard(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1594        let id = req_str(b, "DashboardId")?.to_string();
1595        let mut guard = self.state.write();
1596        let data = guard.get_or_create(&ctx.account);
1597        let d = data
1598            .dashboards
1599            .get_mut(&id)
1600            .ok_or_else(|| dashboard_not_found(&id))?;
1601        if let Some(obj) = d.as_object_mut() {
1602            copy_fields(
1603                b,
1604                obj,
1605                &["Widgets", "RefreshSchedule", "TerminationProtectionEnabled"],
1606            );
1607            obj.insert("UpdatedTimestamp".into(), json!(now_ts()));
1608        }
1609        let d = d.clone();
1610        let mut out = d.as_object().cloned().unwrap_or_default();
1611        out.remove("Status");
1612        out.remove("TagsList");
1613        ok(Value::Object(out))
1614    }
1615
1616    fn delete_dashboard(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1617        let id = req_str(b, "DashboardId")?.to_string();
1618        let mut guard = self.state.write();
1619        let data = guard.get_or_create(&ctx.account);
1620        if data.dashboards.remove(&id).is_none() {
1621            return Err(dashboard_not_found(&id));
1622        }
1623        // Dashboard ARN is name-derived, so destroy+recreate reuses it. Purge
1624        // tags keyed by the ARN so they don't leak onto the recreated dashboard.
1625        data.tags.remove(&id);
1626        empty_ok()
1627    }
1628
1629    fn list_dashboards(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1630        let type_filter = opt_str(b, "Type").map(str::to_string);
1631        let name_prefix = opt_str(b, "NamePrefix").map(str::to_string);
1632        let guard = self.state.read();
1633        let rows: Vec<Value> = guard
1634            .get(&ctx.account)
1635            .map(|d| {
1636                d.dashboards
1637                    .values()
1638                    .filter(|dash| {
1639                        type_filter.as_deref().is_none_or(|want| {
1640                            dash.get("Type").and_then(Value::as_str) == Some(want)
1641                        }) && name_prefix.as_deref().is_none_or(|p| {
1642                            dash.get("Name")
1643                                .and_then(Value::as_str)
1644                                .map(|n| n.starts_with(p))
1645                                .unwrap_or(false)
1646                        })
1647                    })
1648                    .map(|dash| {
1649                        json!({
1650                            "DashboardArn": dash.get("DashboardArn").cloned().unwrap_or(Value::Null),
1651                            "Type": dash.get("Type").cloned().unwrap_or(Value::Null),
1652                        })
1653                    })
1654                    .collect()
1655            })
1656            .unwrap_or_default();
1657        list_response("Dashboards", rows, b)
1658    }
1659
1660    fn start_dashboard_refresh(
1661        &self,
1662        ctx: &Ctx,
1663        b: &Value,
1664    ) -> Result<AwsResponse, AwsServiceError> {
1665        let id = req_str(b, "DashboardId")?.to_string();
1666        let guard = self.state.read();
1667        if !guard
1668            .get(&ctx.account)
1669            .map(|d| d.dashboards.contains_key(&id))
1670            .unwrap_or(false)
1671        {
1672            return Err(dashboard_not_found(&id));
1673        }
1674        ok(json!({ "RefreshId": new_uuid() }))
1675    }
1676}
1677
1678// ===================== resource policy =====================
1679
1680impl CloudTrailService {
1681    fn put_resource_policy(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1682        let arn = req_str(b, "ResourceArn")?.to_string();
1683        let policy = req_str(b, "ResourcePolicy")?.to_string();
1684        let mut guard = self.state.write();
1685        let data = guard.get_or_create(&ctx.account);
1686        data.resource_policies.insert(arn.clone(), policy.clone());
1687        ok(json!({ "ResourceArn": arn, "ResourcePolicy": policy }))
1688    }
1689
1690    fn get_resource_policy(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1691        let arn = req_str(b, "ResourceArn")?.to_string();
1692        let guard = self.state.read();
1693        let policy = guard
1694            .get(&ctx.account)
1695            .and_then(|d| d.resource_policies.get(&arn))
1696            .cloned()
1697            .ok_or_else(|| {
1698                err(
1699                    "ResourcePolicyNotFoundException",
1700                    &format!("No resource policy was found for {arn}."),
1701                )
1702            })?;
1703        ok(json!({ "ResourceArn": arn, "ResourcePolicy": policy }))
1704    }
1705
1706    fn delete_resource_policy(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1707        let arn = req_str(b, "ResourceArn")?.to_string();
1708        let mut guard = self.state.write();
1709        let data = guard.get_or_create(&ctx.account);
1710        if data.resource_policies.remove(&arn).is_none() {
1711            return Err(err(
1712                "ResourcePolicyNotFoundException",
1713                &format!("No resource policy was found for {arn}."),
1714            ));
1715        }
1716        empty_ok()
1717    }
1718}
1719
1720// ===================== org delegated admin =====================
1721
1722impl CloudTrailService {
1723    fn register_delegated_admin(
1724        &self,
1725        ctx: &Ctx,
1726        b: &Value,
1727    ) -> Result<AwsResponse, AwsServiceError> {
1728        let account = req_str(b, "MemberAccountId")?.to_string();
1729        let mut guard = self.state.write();
1730        let data = guard.get_or_create(&ctx.account);
1731        if data.delegated_admins.contains_key(&account) {
1732            return Err(err(
1733                "AccountRegisteredException",
1734                "The account is already registered as the delegated administrator.",
1735            ));
1736        }
1737        data.delegated_admins
1738            .insert(account, json!({ "RegisteredTimestamp": now_ts() }));
1739        empty_ok()
1740    }
1741
1742    fn deregister_delegated_admin(
1743        &self,
1744        ctx: &Ctx,
1745        b: &Value,
1746    ) -> Result<AwsResponse, AwsServiceError> {
1747        let account = req_str(b, "DelegatedAdminAccountId")?.to_string();
1748        let mut guard = self.state.write();
1749        let data = guard.get_or_create(&ctx.account);
1750        if data.delegated_admins.remove(&account).is_none() {
1751            return Err(err(
1752                "AccountNotRegisteredException",
1753                "The account is not registered as the delegated administrator.",
1754            ));
1755        }
1756        empty_ok()
1757    }
1758}
1759
1760// ===================== event configuration =====================
1761
1762impl CloudTrailService {
1763    fn get_event_configuration(
1764        &self,
1765        ctx: &Ctx,
1766        b: &Value,
1767    ) -> Result<AwsResponse, AwsServiceError> {
1768        let (key, is_trail) = self.event_config_target(ctx, b)?;
1769        let guard = self.state.read();
1770        let stored = guard
1771            .get(&ctx.account)
1772            .and_then(|d| d.event_configurations.get(&key))
1773            .cloned();
1774        Ok(AwsResponse::json_value(
1775            StatusCode::OK,
1776            self.event_config_response(&key, is_trail, stored.as_ref(), None),
1777        ))
1778    }
1779
1780    fn put_event_configuration(
1781        &self,
1782        ctx: &Ctx,
1783        b: &Value,
1784    ) -> Result<AwsResponse, AwsServiceError> {
1785        let (key, is_trail) = self.event_config_target(ctx, b)?;
1786        let mut config = Map::new();
1787        copy_fields(
1788            b,
1789            &mut config,
1790            &[
1791                "MaxEventSize",
1792                "ContextKeySelectors",
1793                "AggregationConfigurations",
1794            ],
1795        );
1796        let config = Value::Object(config);
1797        let mut guard = self.state.write();
1798        let data = guard.get_or_create(&ctx.account);
1799        data.event_configurations
1800            .insert(key.clone(), config.clone());
1801        Ok(AwsResponse::json_value(
1802            StatusCode::OK,
1803            self.event_config_response(&key, is_trail, Some(&config), Some(b)),
1804        ))
1805    }
1806
1807    /// Resolve the trail ARN or event-data-store ARN targeted by an event
1808    /// configuration op, returning `(key, is_trail)`.
1809    fn event_config_target(&self, ctx: &Ctx, b: &Value) -> Result<(String, bool), AwsServiceError> {
1810        if let Some(trail) = opt_str(b, "TrailName").filter(|s| !s.is_empty()) {
1811            let name = trail_key(trail).to_string();
1812            let guard = self.state.read();
1813            if !guard
1814                .get(&ctx.account)
1815                .map(|d| d.trails.contains_key(&name))
1816                .unwrap_or(false)
1817            {
1818                return Err(trail_not_found(&name));
1819            }
1820            return Ok((trail_arn(ctx, &name), true));
1821        }
1822        if let Some(eds) = opt_str(b, "EventDataStore").filter(|s| !s.is_empty()) {
1823            let guard = self.state.read();
1824            if !guard
1825                .get(&ctx.account)
1826                .map(|d| d.event_data_stores.contains_key(eds))
1827                .unwrap_or(false)
1828            {
1829                return Err(eds_not_found(eds));
1830            }
1831            return Ok((eds.to_string(), false));
1832        }
1833        Err(err(
1834            "InvalidParameterCombinationException",
1835            "Either TrailName or EventDataStore must be specified.",
1836        ))
1837    }
1838
1839    fn event_config_response(
1840        &self,
1841        key: &str,
1842        is_trail: bool,
1843        stored: Option<&Value>,
1844        input: Option<&Value>,
1845    ) -> Value {
1846        let mut out = Map::new();
1847        if is_trail {
1848            out.insert("TrailARN".into(), json!(key));
1849        } else {
1850            out.insert("EventDataStoreArn".into(), json!(key));
1851        }
1852        let source = input.or(stored);
1853        out.insert(
1854            "MaxEventSize".into(),
1855            source
1856                .and_then(|s| s.get("MaxEventSize"))
1857                .cloned()
1858                .unwrap_or(json!("Standard")),
1859        );
1860        out.insert(
1861            "ContextKeySelectors".into(),
1862            source
1863                .and_then(|s| s.get("ContextKeySelectors"))
1864                .cloned()
1865                .unwrap_or(json!([])),
1866        );
1867        out.insert(
1868            "AggregationConfigurations".into(),
1869            source
1870                .and_then(|s| s.get("AggregationConfigurations"))
1871                .cloned()
1872                .unwrap_or(json!([])),
1873        );
1874        Value::Object(out)
1875    }
1876}
1877
1878// ===================== tagging =====================
1879
1880impl CloudTrailService {
1881    fn add_tags(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1882        let id = req_str(b, "ResourceId")?.to_string();
1883        b.get("TagsList")
1884            .and_then(Value::as_array)
1885            .ok_or_else(|| invalid_param("TagsList is required."))?;
1886        let mut guard = self.state.write();
1887        let data = guard.get_or_create(&ctx.account);
1888        store_tags(data, &id, b);
1889        empty_ok()
1890    }
1891
1892    fn remove_tags(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1893        let id = req_str(b, "ResourceId")?.to_string();
1894        let tags = b
1895            .get("TagsList")
1896            .and_then(Value::as_array)
1897            .ok_or_else(|| invalid_param("TagsList is required."))?
1898            .clone();
1899        let mut guard = self.state.write();
1900        let data = guard.get_or_create(&ctx.account);
1901        if let Some(entry) = data.tags.get_mut(&id) {
1902            for t in &tags {
1903                if let Some(k) = t.get("Key").and_then(Value::as_str) {
1904                    entry.remove(k);
1905                }
1906            }
1907        }
1908        empty_ok()
1909    }
1910
1911    fn list_tags(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1912        let ids = b
1913            .get("ResourceIdList")
1914            .and_then(Value::as_array)
1915            .ok_or_else(|| invalid_param("ResourceIdList is required."))?;
1916        let guard = self.state.read();
1917        let data = guard.get(&ctx.account);
1918        let mut list = Vec::new();
1919        for id in ids {
1920            let Some(id) = id.as_str() else { continue };
1921            let tags: Vec<Value> = data
1922                .and_then(|d| d.tags.get(id))
1923                .map(|m| {
1924                    m.iter()
1925                        .map(|(k, v)| json!({ "Key": k, "Value": v }))
1926                        .collect()
1927                })
1928                .unwrap_or_default();
1929            list.push(json!({ "ResourceId": id, "TagsList": tags }));
1930        }
1931        ok(json!({ "ResourceTagList": list }))
1932    }
1933}
1934
1935// ===================== read-only / empty result ops =====================
1936
1937fn lookup_events(b: &Value) -> Result<AwsResponse, AwsServiceError> {
1938    // A fake needn't record its own API activity; return an empty, real page.
1939    if let Some(cat) = opt_str(b, "EventCategory") {
1940        if cat != "insight" {
1941            return Err(err(
1942                "InvalidEventCategoryException",
1943                &format!("{cat} is not a valid event category."),
1944            ));
1945        }
1946    }
1947    ok(json!({ "Events": [] }))
1948}
1949
1950fn list_public_keys() -> Result<AwsResponse, AwsServiceError> {
1951    ok(json!({ "PublicKeyList": [] }))
1952}
1953
1954fn list_insights_data(b: &Value) -> Result<AwsResponse, AwsServiceError> {
1955    req_str(b, "InsightSource")?;
1956    req_str(b, "DataType")?;
1957    ok(json!({ "Events": [] }))
1958}
1959
1960fn list_insights_metric_data(b: &Value) -> Result<AwsResponse, AwsServiceError> {
1961    let event_source = req_str(b, "EventSource")?.to_string();
1962    let event_name = req_str(b, "EventName")?.to_string();
1963    let insight_type = req_str(b, "InsightType")?.to_string();
1964    let mut out = Map::new();
1965    out.insert("EventSource".into(), json!(event_source));
1966    out.insert("EventName".into(), json!(event_name));
1967    out.insert("InsightType".into(), json!(insight_type));
1968    if let Some(ec) = opt_str(b, "ErrorCode") {
1969        out.insert("ErrorCode".into(), json!(ec));
1970    }
1971    out.insert("Timestamps".into(), json!([]));
1972    out.insert("Values".into(), json!([]));
1973    ok(Value::Object(out))
1974}
1975
1976fn search_sample_queries(b: &Value) -> Result<AwsResponse, AwsServiceError> {
1977    req_str(b, "SearchPhrase")?;
1978    ok(json!({ "SearchResults": [] }))
1979}