Skip to main content

fakecloud_transfer/
service.rs

1//! AWS Transfer Family (`transfer`) awsJson1.1 dispatch + operation handlers.
2//!
3//! The full 71-operation Transfer Family control plane. State is
4//! account-partitioned and persisted. Each resource is stored as its
5//! already-output-valid `DescribedX` JSON object so `Describe`/`List` echo
6//! exactly what `Create`/`Import`/`Update` persisted, and nested configuration
7//! objects round-trip verbatim.
8
9use std::sync::Arc;
10
11use async_trait::async_trait;
12use http::StatusCode;
13use serde_json::{json, Map, Value};
14use tokio::sync::Mutex as AsyncMutex;
15use uuid::Uuid;
16
17use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
18use fakecloud_persistence::SnapshotStore;
19
20use crate::persistence::save_snapshot;
21use crate::state::{SharedTransferState, TransferData};
22
23/// Every operation name in the Transfer Family Smithy model (71 operations).
24pub const TRANSFER_ACTIONS: &[&str] = &[
25    "CreateAccess",
26    "CreateAgreement",
27    "CreateConnector",
28    "CreateProfile",
29    "CreateServer",
30    "CreateUser",
31    "CreateWebApp",
32    "CreateWorkflow",
33    "DeleteAccess",
34    "DeleteAgreement",
35    "DeleteCertificate",
36    "DeleteConnector",
37    "DeleteHostKey",
38    "DeleteProfile",
39    "DeleteServer",
40    "DeleteSshPublicKey",
41    "DeleteUser",
42    "DeleteWebApp",
43    "DeleteWebAppCustomization",
44    "DeleteWorkflow",
45    "DescribeAccess",
46    "DescribeAgreement",
47    "DescribeCertificate",
48    "DescribeConnector",
49    "DescribeExecution",
50    "DescribeHostKey",
51    "DescribeProfile",
52    "DescribeSecurityPolicy",
53    "DescribeServer",
54    "DescribeUser",
55    "DescribeWebApp",
56    "DescribeWebAppCustomization",
57    "DescribeWorkflow",
58    "ImportCertificate",
59    "ImportHostKey",
60    "ImportSshPublicKey",
61    "ListAccesses",
62    "ListAgreements",
63    "ListCertificates",
64    "ListConnectors",
65    "ListExecutions",
66    "ListFileTransferResults",
67    "ListHostKeys",
68    "ListProfiles",
69    "ListSecurityPolicies",
70    "ListServers",
71    "ListTagsForResource",
72    "ListUsers",
73    "ListWebApps",
74    "ListWorkflows",
75    "SendWorkflowStepState",
76    "StartDirectoryListing",
77    "StartFileTransfer",
78    "StartRemoteDelete",
79    "StartRemoteMove",
80    "StartServer",
81    "StopServer",
82    "TagResource",
83    "TestConnection",
84    "TestIdentityProvider",
85    "UntagResource",
86    "UpdateAccess",
87    "UpdateAgreement",
88    "UpdateCertificate",
89    "UpdateConnector",
90    "UpdateHostKey",
91    "UpdateProfile",
92    "UpdateServer",
93    "UpdateUser",
94    "UpdateWebApp",
95    "UpdateWebAppCustomization",
96];
97
98pub struct TransferService {
99    state: SharedTransferState,
100    snapshot_store: Option<Arc<dyn SnapshotStore>>,
101    snapshot_lock: Arc<AsyncMutex<()>>,
102}
103
104impl TransferService {
105    pub fn new(state: SharedTransferState) -> Self {
106        Self {
107            state,
108            snapshot_store: None,
109            snapshot_lock: Arc::new(AsyncMutex::new(())),
110        }
111    }
112
113    pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
114        self.snapshot_store = Some(store);
115        self
116    }
117
118    async fn save(&self) {
119        save_snapshot(
120            &self.state,
121            self.snapshot_store.clone(),
122            &self.snapshot_lock,
123        )
124        .await;
125    }
126}
127
128#[async_trait]
129impl AwsService for TransferService {
130    fn service_name(&self) -> &str {
131        "transfer"
132    }
133
134    async fn handle(&self, request: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
135        let mutates = is_mutating(request.action.as_str());
136        let result = dispatch(self, &request);
137        if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
138            self.save().await;
139        }
140        result
141    }
142
143    fn supported_actions(&self) -> &[&str] {
144        TRANSFER_ACTIONS
145    }
146}
147
148/// Read/test operations never mutate state. Everything else (Create, Import,
149/// Update, Delete, Start, Stop, Send, Tag, Untag) can, so it triggers a
150/// snapshot save on success.
151fn is_mutating(action: &str) -> bool {
152    !(action.starts_with("Describe") || action.starts_with("List") || action.starts_with("Test"))
153}
154
155fn dispatch(s: &TransferService, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
156    let b = parse(req)?;
157    crate::validate::validate_input(req.action.as_str(), &b)?;
158    let ctx = Ctx {
159        account: req.account_id.clone(),
160        region: req.region.clone(),
161    };
162    match req.action.as_str() {
163        // servers
164        "CreateServer" => s.create_server(&ctx, &b),
165        "DescribeServer" => s.describe_server(&ctx, &b),
166        "UpdateServer" => s.update_server(&ctx, &b),
167        "DeleteServer" => s.delete_server(&ctx, &b),
168        "StartServer" => s.start_server(&ctx, &b),
169        "StopServer" => s.stop_server(&ctx, &b),
170        "ListServers" => s.list_servers(&ctx, &b),
171        // users
172        "CreateUser" => s.create_user(&ctx, &b),
173        "DescribeUser" => s.describe_user(&ctx, &b),
174        "UpdateUser" => s.update_user(&ctx, &b),
175        "DeleteUser" => s.delete_user(&ctx, &b),
176        "ListUsers" => s.list_users(&ctx, &b),
177        // ssh public keys
178        "ImportSshPublicKey" => s.import_ssh_public_key(&ctx, &b),
179        "DeleteSshPublicKey" => s.delete_ssh_public_key(&ctx, &b),
180        // host keys
181        "ImportHostKey" => s.import_host_key(&ctx, &b),
182        "UpdateHostKey" => s.update_host_key(&ctx, &b),
183        "DescribeHostKey" => s.describe_host_key(&ctx, &b),
184        "DeleteHostKey" => s.delete_host_key(&ctx, &b),
185        "ListHostKeys" => s.list_host_keys(&ctx, &b),
186        // accesses
187        "CreateAccess" => s.create_access(&ctx, &b),
188        "UpdateAccess" => s.update_access(&ctx, &b),
189        "DescribeAccess" => s.describe_access(&ctx, &b),
190        "DeleteAccess" => s.delete_access(&ctx, &b),
191        "ListAccesses" => s.list_accesses(&ctx, &b),
192        // workflows
193        "CreateWorkflow" => s.create_workflow(&ctx, &b),
194        "DescribeWorkflow" => s.describe_workflow(&ctx, &b),
195        "DeleteWorkflow" => s.delete_workflow(&ctx, &b),
196        "ListWorkflows" => s.list_workflows(&ctx, &b),
197        "SendWorkflowStepState" => s.send_workflow_step_state(&ctx, &b),
198        // executions
199        "DescribeExecution" => s.describe_execution(&ctx, &b),
200        "ListExecutions" => s.list_executions(&ctx, &b),
201        // agreements
202        "CreateAgreement" => s.create_agreement(&ctx, &b),
203        "UpdateAgreement" => s.update_agreement(&ctx, &b),
204        "DescribeAgreement" => s.describe_agreement(&ctx, &b),
205        "DeleteAgreement" => s.delete_agreement(&ctx, &b),
206        "ListAgreements" => s.list_agreements(&ctx, &b),
207        // connectors
208        "CreateConnector" => s.create_connector(&ctx, &b),
209        "UpdateConnector" => s.update_connector(&ctx, &b),
210        "DescribeConnector" => s.describe_connector(&ctx, &b),
211        "DeleteConnector" => s.delete_connector(&ctx, &b),
212        "ListConnectors" => s.list_connectors(&ctx, &b),
213        "TestConnection" => s.test_connection(&ctx, &b),
214        "StartDirectoryListing" => s.start_directory_listing(&ctx, &b),
215        "StartFileTransfer" => s.start_file_transfer(&ctx, &b),
216        "StartRemoteDelete" => s.start_remote_delete(&ctx, &b),
217        "StartRemoteMove" => s.start_remote_move(&ctx, &b),
218        "ListFileTransferResults" => s.list_file_transfer_results(&ctx, &b),
219        // profiles
220        "CreateProfile" => s.create_profile(&ctx, &b),
221        "UpdateProfile" => s.update_profile(&ctx, &b),
222        "DescribeProfile" => s.describe_profile(&ctx, &b),
223        "DeleteProfile" => s.delete_profile(&ctx, &b),
224        "ListProfiles" => s.list_profiles(&ctx, &b),
225        // certificates
226        "ImportCertificate" => s.import_certificate(&ctx, &b),
227        "UpdateCertificate" => s.update_certificate(&ctx, &b),
228        "DescribeCertificate" => s.describe_certificate(&ctx, &b),
229        "DeleteCertificate" => s.delete_certificate(&ctx, &b),
230        "ListCertificates" => s.list_certificates(&ctx, &b),
231        // security policies
232        "DescribeSecurityPolicy" => s.describe_security_policy(&b),
233        "ListSecurityPolicies" => s.list_security_policies(&b),
234        // web apps
235        "CreateWebApp" => s.create_web_app(&ctx, &b),
236        "UpdateWebApp" => s.update_web_app(&ctx, &b),
237        "DescribeWebApp" => s.describe_web_app(&ctx, &b),
238        "DeleteWebApp" => s.delete_web_app(&ctx, &b),
239        "ListWebApps" => s.list_web_apps(&ctx, &b),
240        "UpdateWebAppCustomization" => s.update_web_app_customization(&ctx, &b),
241        "DescribeWebAppCustomization" => s.describe_web_app_customization(&ctx, &b),
242        "DeleteWebAppCustomization" => s.delete_web_app_customization(&ctx, &b),
243        // identity provider
244        "TestIdentityProvider" => s.test_identity_provider(&ctx, &b),
245        // tagging
246        "TagResource" => s.tag_resource(&ctx, &b),
247        "UntagResource" => s.untag_resource(&ctx, &b),
248        "ListTagsForResource" => s.list_tags_for_resource(&ctx, &b),
249        _ => Err(AwsServiceError::action_not_implemented(
250            s.service_name(),
251            &req.action,
252        )),
253    }
254}
255
256// ===================== helpers =====================
257
258/// Per-request account + region context.
259struct Ctx {
260    account: String,
261    region: String,
262}
263
264fn ok(v: Value) -> Result<AwsResponse, AwsServiceError> {
265    Ok(AwsResponse::json_value(StatusCode::OK, v))
266}
267
268fn parse(req: &AwsRequest) -> Result<Value, AwsServiceError> {
269    if req.body.is_empty() {
270        return Ok(json!({}));
271    }
272    serde_json::from_slice(&req.body)
273        .map_err(|e| invalid_request(&format!("Request body is malformed: {e}")))
274}
275
276fn invalid_request(msg: &str) -> AwsServiceError {
277    AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "InvalidRequestException", msg)
278}
279
280fn not_found(msg: &str) -> AwsServiceError {
281    AwsServiceError::aws_error(StatusCode::NOT_FOUND, "ResourceNotFoundException", msg)
282}
283
284fn resource_exists(msg: &str) -> AwsServiceError {
285    // The Smithy model marks `ResourceExistsException` with `httpError: 409`.
286    AwsServiceError::aws_error(StatusCode::CONFLICT, "ResourceExistsException", msg)
287}
288
289/// Read a required, non-empty string field.
290fn req_str<'a>(b: &'a Value, f: &str) -> Result<&'a str, AwsServiceError> {
291    b.get(f)
292        .and_then(Value::as_str)
293        .filter(|s| !s.is_empty())
294        .ok_or_else(|| invalid_request(&format!("{f} is required.")))
295}
296
297fn opt_str<'a>(b: &'a Value, f: &str) -> Option<&'a str> {
298    b.get(f).and_then(Value::as_str).filter(|s| !s.is_empty())
299}
300
301fn now_ts() -> f64 {
302    chrono::Utc::now().timestamp() as f64
303}
304
305/// 17 lowercase hex characters, matching Transfer's resource-id suffix form.
306fn hex17() -> String {
307    let raw = Uuid::new_v4().simple().to_string();
308    raw.chars()
309        .filter(|c| c.is_ascii_hexdigit())
310        .take(17)
311        .collect()
312}
313
314fn arn(ctx: &Ctx, path: &str) -> String {
315    format!("arn:aws:transfer:{}:{}:{}", ctx.region, ctx.account, path)
316}
317
318/// Copy the given fields verbatim from `src` into `dst` when present.
319fn copy_present(src: &Value, dst: &mut Map<String, Value>, fields: &[&str]) {
320    for f in fields {
321        if let Some(v) = src.get(*f) {
322            dst.insert((*f).to_string(), v.clone());
323        }
324    }
325}
326
327/// Collect a resource's `Tags` input into the account tag map under its ARN.
328fn store_tags(data: &mut TransferData, arn: &str, b: &Value) {
329    if let Some(tags) = b.get("Tags").and_then(Value::as_array) {
330        let entry = data.tags.entry(arn.to_string()).or_default();
331        for t in tags {
332            if let (Some(k), Some(v)) = (
333                t.get("Key").and_then(Value::as_str),
334                t.get("Value").and_then(Value::as_str),
335            ) {
336                entry.insert(k.to_string(), v.to_string());
337            }
338        }
339    }
340}
341
342/// Return a resource's `Tags` list (from the account tag map) for embedding in
343/// a `Describe` response, or `None` when the resource carries no tags.
344fn tags_list(data: &TransferData, arn: &str) -> Option<Value> {
345    data.tags.get(arn).filter(|m| !m.is_empty()).map(|m| {
346        Value::Array(
347            m.iter()
348                .map(|(k, v)| json!({ "Key": k, "Value": v }))
349                .collect(),
350        )
351    })
352}
353
354/// Paginate an ordered list of rows using Transfer's `MaxResults` +
355/// `NextToken` (opaque start-index) window.
356fn paginate(rows: Vec<Value>, b: &Value) -> (Vec<Value>, Option<String>) {
357    let start = b
358        .get("NextToken")
359        .and_then(Value::as_str)
360        .and_then(|t| t.parse::<usize>().ok())
361        .unwrap_or(0);
362    let max = b
363        .get("MaxResults")
364        .and_then(Value::as_u64)
365        .map(|m| m.clamp(1, 1000) as usize)
366        .unwrap_or(1000);
367    let end = start.saturating_add(max).min(rows.len());
368    let page = rows.get(start..end).unwrap_or(&[]).to_vec();
369    let next = if end < rows.len() {
370        Some(end.to_string())
371    } else {
372        None
373    };
374    (page, next)
375}
376
377/// Build a `{key: [...], NextToken?: ...}` list response with optional extra
378/// top-level fields (e.g. the echoed `ServerId`).
379fn list_response(key: &str, rows: Vec<Value>, b: &Value, extra: &[(&str, Value)]) -> Value {
380    let (page, next) = paginate(rows, b);
381    let mut out = Map::new();
382    for (k, v) in extra {
383        out.insert((*k).to_string(), v.clone());
384    }
385    out.insert(key.to_string(), Value::Array(page));
386    if let Some(t) = next {
387        out.insert("NextToken".to_string(), json!(t));
388    }
389    Value::Object(out)
390}
391
392// ===================== servers =====================
393
394/// Input fields that echo verbatim into a `DescribedServer`.
395const SERVER_FIELDS: &[&str] = &[
396    "Certificate",
397    "ProtocolDetails",
398    "EndpointDetails",
399    "EndpointType",
400    "IdentityProviderDetails",
401    "IdentityProviderType",
402    "LoggingRole",
403    "PostAuthenticationLoginBanner",
404    "PreAuthenticationLoginBanner",
405    "Protocols",
406    "SecurityPolicyName",
407    "WorkflowDetails",
408    "StructuredLogDestinations",
409    "S3StorageOptions",
410    "IpAddressType",
411    "Domain",
412];
413
414/// Fields an `UpdateServer` request may mutate. Mirrors `SERVER_FIELDS` but
415/// excludes create-only members (`Domain`) that are absent from the model's
416/// `UpdateServerRequest` shape, so an `UpdateServer` carrying `Domain` cannot
417/// silently rewrite an immutable attribute.
418const UPDATE_SERVER_FIELDS: &[&str] = &[
419    "Certificate",
420    "ProtocolDetails",
421    "EndpointDetails",
422    "EndpointType",
423    "IdentityProviderDetails",
424    "IdentityProviderType",
425    "LoggingRole",
426    "PostAuthenticationLoginBanner",
427    "PreAuthenticationLoginBanner",
428    "Protocols",
429    "SecurityPolicyName",
430    "WorkflowDetails",
431    "StructuredLogDestinations",
432    "S3StorageOptions",
433    "IpAddressType",
434];
435
436impl TransferService {
437    fn require_server<'a>(
438        &self,
439        data: &'a TransferData,
440        server_id: &str,
441    ) -> Result<&'a Value, AwsServiceError> {
442        data.servers
443            .get(server_id)
444            .ok_or_else(|| not_found(&format!("Server {server_id} does not exist.")))
445    }
446
447    fn create_server(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
448        let server_id = format!("s-{}", hex17());
449        let server_arn = arn(ctx, &format!("server/{server_id}"));
450        let mut srv = Map::new();
451        srv.insert("Arn".into(), json!(server_arn));
452        srv.insert("ServerId".into(), json!(server_id));
453        srv.insert(
454            "Domain".into(),
455            b.get("Domain").cloned().unwrap_or(json!("S3")),
456        );
457        srv.insert(
458            "EndpointType".into(),
459            b.get("EndpointType").cloned().unwrap_or(json!("PUBLIC")),
460        );
461        srv.insert(
462            "IdentityProviderType".into(),
463            b.get("IdentityProviderType")
464                .cloned()
465                .unwrap_or(json!("SERVICE_MANAGED")),
466        );
467        // AWS defaults `Protocols` to `["SFTP"]` and `DescribedServer` always
468        // echoes it, so default it here to avoid terraform drift.
469        srv.insert(
470            "Protocols".into(),
471            b.get("Protocols").cloned().unwrap_or(json!(["SFTP"])),
472        );
473        // Settle to ONLINE immediately so `server-online` waiters complete
474        // against the synchronous control plane.
475        srv.insert("State".into(), json!("ONLINE"));
476        srv.insert("UserCount".into(), json!(0));
477        srv.insert(
478            "HostKeyFingerprint".into(),
479            json!(format!("SHA256:{}", hex17())),
480        );
481        copy_present(b, &mut srv, SERVER_FIELDS);
482        let srv = Value::Object(srv);
483        let mut guard = self.state.write();
484        let data = guard.get_or_create(&ctx.account);
485        data.servers.insert(server_id.clone(), srv);
486        store_tags(data, &server_arn, b);
487        ok(json!({ "ServerId": server_id }))
488    }
489
490    fn describe_server(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
491        let server_id = req_str(b, "ServerId")?;
492        let guard = self.state.read();
493        let data = guard
494            .get(&ctx.account)
495            .ok_or_else(|| not_found(&format!("Server {server_id} does not exist.")))?;
496        let srv = self.require_server(data, server_id)?;
497        let mut out = srv.as_object().unwrap().clone();
498        let arn = out
499            .get("Arn")
500            .and_then(Value::as_str)
501            .unwrap_or("")
502            .to_string();
503        if let Some(tags) = tags_list(data, &arn) {
504            out.insert("Tags".into(), tags);
505        }
506        // Reflect the live user count.
507        let count = data
508            .users
509            .keys()
510            .filter(|k| k.starts_with(&format!("{server_id}/")))
511            .count();
512        out.insert("UserCount".into(), json!(count));
513        ok(json!({ "Server": Value::Object(out) }))
514    }
515
516    fn update_server(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
517        let server_id = req_str(b, "ServerId")?.to_string();
518        let mut guard = self.state.write();
519        let data = guard.get_or_create(&ctx.account);
520        let srv = data
521            .servers
522            .get_mut(&server_id)
523            .ok_or_else(|| not_found(&format!("Server {server_id} does not exist.")))?;
524        let obj = srv.as_object_mut().unwrap();
525        copy_present(b, obj, UPDATE_SERVER_FIELDS);
526        ok(json!({ "ServerId": server_id }))
527    }
528
529    fn delete_server(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
530        let server_id = req_str(b, "ServerId")?.to_string();
531        let mut guard = self.state.write();
532        let data = guard.get_or_create(&ctx.account);
533        let srv = data
534            .servers
535            .remove(&server_id)
536            .ok_or_else(|| not_found(&format!("Server {server_id} does not exist.")))?;
537        if let Some(arn) = srv.get("Arn").and_then(Value::as_str) {
538            data.tags.remove(arn);
539        }
540        let prefix = format!("{server_id}/");
541        // Collect the ARNs of every cascaded child so their tag entries are
542        // removed too (otherwise deleting the server leaks child tags).
543        let mut child_arns: Vec<String> = Vec::new();
544        for map in [
545            &data.users,
546            &data.accesses,
547            &data.host_keys,
548            &data.agreements,
549        ] {
550            for (k, v) in map.iter() {
551                if k.starts_with(&prefix) {
552                    if let Some(arn) = v.get("Arn").and_then(Value::as_str) {
553                        child_arns.push(arn.to_string());
554                    }
555                }
556            }
557        }
558        for arn in &child_arns {
559            data.tags.remove(arn);
560        }
561        data.users.retain(|k, _| !k.starts_with(&prefix));
562        data.accesses.retain(|k, _| !k.starts_with(&prefix));
563        data.host_keys.retain(|k, _| !k.starts_with(&prefix));
564        data.agreements.retain(|k, _| !k.starts_with(&prefix));
565        ok(json!({}))
566    }
567
568    fn start_server(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
569        self.set_server_state(ctx, b, "ONLINE")
570    }
571
572    fn stop_server(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
573        self.set_server_state(ctx, b, "OFFLINE")
574    }
575
576    fn set_server_state(
577        &self,
578        ctx: &Ctx,
579        b: &Value,
580        state: &str,
581    ) -> Result<AwsResponse, AwsServiceError> {
582        let server_id = req_str(b, "ServerId")?.to_string();
583        let mut guard = self.state.write();
584        let data = guard.get_or_create(&ctx.account);
585        let srv = data
586            .servers
587            .get_mut(&server_id)
588            .ok_or_else(|| not_found(&format!("Server {server_id} does not exist.")))?;
589        // Settle synchronously to the requested steady state so a describe
590        // waiter observes ONLINE/OFFLINE immediately.
591        srv.as_object_mut()
592            .unwrap()
593            .insert("State".into(), json!(state));
594        ok(json!({}))
595    }
596
597    fn list_servers(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
598        let guard = self.state.read();
599        let rows = guard
600            .get(&ctx.account)
601            .map(|d| d.servers.values().map(|s| listed_server(s, d)).collect())
602            .unwrap_or_default();
603        ok(list_response("Servers", rows, b, &[]))
604    }
605}
606
607fn listed_server(srv: &Value, data: &TransferData) -> Value {
608    let server_id = srv.get("ServerId").and_then(Value::as_str).unwrap_or("");
609    let count = data
610        .users
611        .keys()
612        .filter(|k| k.starts_with(&format!("{server_id}/")))
613        .count();
614    let mut out = Map::new();
615    for f in [
616        "Arn",
617        "Domain",
618        "IdentityProviderType",
619        "EndpointType",
620        "LoggingRole",
621        "ServerId",
622        "State",
623    ] {
624        if let Some(v) = srv.get(f) {
625            out.insert(f.to_string(), v.clone());
626        }
627    }
628    out.insert("UserCount".into(), json!(count));
629    Value::Object(out)
630}
631
632// ===================== users + ssh keys =====================
633
634const USER_FIELDS: &[&str] = &[
635    "HomeDirectory",
636    "HomeDirectoryMappings",
637    "HomeDirectoryType",
638    "Policy",
639    "PosixProfile",
640    "Role",
641];
642
643impl TransferService {
644    fn create_user(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
645        let server_id = req_str(b, "ServerId")?.to_string();
646        let user_name = req_str(b, "UserName")?.to_string();
647        req_str(b, "Role")?;
648        let mut guard = self.state.write();
649        let data = guard.get_or_create(&ctx.account);
650        self.require_server(data, &server_id)?;
651        let key = format!("{server_id}/{user_name}");
652        if data.users.contains_key(&key) {
653            return Err(resource_exists(&format!(
654                "User {user_name} already exists on server {server_id}."
655            )));
656        }
657        let user_arn = arn(ctx, &format!("user/{server_id}/{user_name}"));
658        let mut user = Map::new();
659        user.insert("Arn".into(), json!(user_arn));
660        user.insert("UserName".into(), json!(user_name));
661        copy_present(b, &mut user, USER_FIELDS);
662        let mut ssh_keys = Vec::new();
663        if let Some(body) = opt_str(b, "SshPublicKeyBody") {
664            ssh_keys.push(json!({
665                "SshPublicKeyId": format!("key-{}", hex17()),
666                "SshPublicKeyBody": body,
667                "DateImported": now_ts()
668            }));
669        }
670        user.insert("SshPublicKeys".into(), json!(ssh_keys));
671        data.users.insert(key, Value::Object(user));
672        store_tags(data, &user_arn, b);
673        ok(json!({ "ServerId": server_id, "UserName": user_name }))
674    }
675
676    fn describe_user(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
677        let server_id = req_str(b, "ServerId")?.to_string();
678        let user_name = req_str(b, "UserName")?.to_string();
679        let guard = self.state.read();
680        let data = guard
681            .get(&ctx.account)
682            .ok_or_else(|| not_found(&format!("Server {server_id} does not exist.")))?;
683        self.require_server(data, &server_id)?;
684        let user = data
685            .users
686            .get(&format!("{server_id}/{user_name}"))
687            .ok_or_else(|| not_found(&format!("User {user_name} does not exist.")))?;
688        let mut out = user.as_object().unwrap().clone();
689        let arn = out
690            .get("Arn")
691            .and_then(Value::as_str)
692            .unwrap_or("")
693            .to_string();
694        if let Some(tags) = tags_list(data, &arn) {
695            out.insert("Tags".into(), tags);
696        }
697        ok(json!({ "ServerId": server_id, "User": Value::Object(out) }))
698    }
699
700    fn update_user(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
701        let server_id = req_str(b, "ServerId")?.to_string();
702        let user_name = req_str(b, "UserName")?.to_string();
703        let mut guard = self.state.write();
704        let data = guard.get_or_create(&ctx.account);
705        self.require_server(data, &server_id)?;
706        let user = data
707            .users
708            .get_mut(&format!("{server_id}/{user_name}"))
709            .ok_or_else(|| not_found(&format!("User {user_name} does not exist.")))?;
710        copy_present(b, user.as_object_mut().unwrap(), USER_FIELDS);
711        ok(json!({ "ServerId": server_id, "UserName": user_name }))
712    }
713
714    fn delete_user(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
715        let server_id = req_str(b, "ServerId")?.to_string();
716        let user_name = req_str(b, "UserName")?.to_string();
717        let mut guard = self.state.write();
718        let data = guard.get_or_create(&ctx.account);
719        self.require_server(data, &server_id)?;
720        let user = data
721            .users
722            .remove(&format!("{server_id}/{user_name}"))
723            .ok_or_else(|| not_found(&format!("User {user_name} does not exist.")))?;
724        if let Some(arn) = user.get("Arn").and_then(Value::as_str) {
725            data.tags.remove(arn);
726        }
727        ok(json!({}))
728    }
729
730    fn list_users(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
731        let server_id = req_str(b, "ServerId")?.to_string();
732        let guard = self.state.read();
733        let data = guard
734            .get(&ctx.account)
735            .ok_or_else(|| not_found(&format!("Server {server_id} does not exist.")))?;
736        self.require_server(data, &server_id)?;
737        let prefix = format!("{server_id}/");
738        let rows: Vec<Value> = data
739            .users
740            .iter()
741            .filter(|(k, _)| k.starts_with(&prefix))
742            .map(|(_, u)| listed_user(u))
743            .collect();
744        ok(list_response(
745            "Users",
746            rows,
747            b,
748            &[("ServerId", json!(server_id))],
749        ))
750    }
751
752    fn import_ssh_public_key(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
753        let server_id = req_str(b, "ServerId")?.to_string();
754        let user_name = req_str(b, "UserName")?.to_string();
755        let body = req_str(b, "SshPublicKeyBody")?.to_string();
756        let mut guard = self.state.write();
757        let data = guard.get_or_create(&ctx.account);
758        self.require_server(data, &server_id)?;
759        let user = data
760            .users
761            .get_mut(&format!("{server_id}/{user_name}"))
762            .ok_or_else(|| not_found(&format!("User {user_name} does not exist.")))?;
763        let key_id = format!("key-{}", hex17());
764        let keys = user
765            .as_object_mut()
766            .unwrap()
767            .entry("SshPublicKeys")
768            .or_insert_with(|| json!([]));
769        if let Some(arr) = keys.as_array_mut() {
770            if arr
771                .iter()
772                .any(|k| k.get("SshPublicKeyBody").and_then(Value::as_str) == Some(body.as_str()))
773            {
774                return Err(resource_exists("The SSH public key already exists."));
775            }
776            arr.push(json!({
777                "SshPublicKeyId": key_id,
778                "SshPublicKeyBody": body,
779                "DateImported": now_ts()
780            }));
781        }
782        ok(json!({
783            "ServerId": server_id,
784            "SshPublicKeyId": key_id,
785            "UserName": user_name
786        }))
787    }
788
789    fn delete_ssh_public_key(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
790        let server_id = req_str(b, "ServerId")?.to_string();
791        let user_name = req_str(b, "UserName")?.to_string();
792        let key_id = req_str(b, "SshPublicKeyId")?.to_string();
793        let mut guard = self.state.write();
794        let data = guard.get_or_create(&ctx.account);
795        self.require_server(data, &server_id)?;
796        let user = data
797            .users
798            .get_mut(&format!("{server_id}/{user_name}"))
799            .ok_or_else(|| not_found(&format!("User {user_name} does not exist.")))?;
800        let keys = user
801            .as_object_mut()
802            .unwrap()
803            .get_mut("SshPublicKeys")
804            .and_then(Value::as_array_mut);
805        let removed = match keys {
806            Some(arr) => {
807                let before = arr.len();
808                arr.retain(|k| {
809                    k.get("SshPublicKeyId").and_then(Value::as_str) != Some(key_id.as_str())
810                });
811                arr.len() != before
812            }
813            None => false,
814        };
815        if !removed {
816            return Err(not_found(&format!(
817                "SSH public key {key_id} does not exist."
818            )));
819        }
820        ok(json!({}))
821    }
822}
823
824fn listed_user(user: &Value) -> Value {
825    let mut out = Map::new();
826    for f in [
827        "Arn",
828        "HomeDirectory",
829        "HomeDirectoryType",
830        "Role",
831        "UserName",
832    ] {
833        if let Some(v) = user.get(f) {
834            out.insert(f.to_string(), v.clone());
835        }
836    }
837    let count = user
838        .get("SshPublicKeys")
839        .and_then(Value::as_array)
840        .map(|a| a.len())
841        .unwrap_or(0);
842    out.insert("SshPublicKeyCount".into(), json!(count));
843    Value::Object(out)
844}
845
846// ===================== host keys =====================
847
848/// Derive the `HostKeyType` AWS reports from the supplied key material. AWS
849/// inspects the algorithm embedded in the (private or public) key body; we
850/// detect the algorithm token pragmatically and fall back to `ssh-rsa`.
851fn host_key_type(body: &str) -> &'static str {
852    // The algorithm token appears verbatim in an OpenSSH public key line and in
853    // the header/blob of a private key. Probe the more specific tokens first;
854    // RSA (`ssh-rsa`, PEM `BEGIN RSA`, generic OpenSSH) is the default.
855    for token in [
856        "ssh-ed25519",
857        "ecdsa-sha2-nistp256",
858        "ecdsa-sha2-nistp384",
859        "ecdsa-sha2-nistp521",
860    ] {
861        if body.contains(token) {
862            return token;
863        }
864    }
865    "ssh-rsa"
866}
867
868impl TransferService {
869    fn import_host_key(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
870        let server_id = req_str(b, "ServerId")?.to_string();
871        let host_key_body = req_str(b, "HostKeyBody")?.to_string();
872        let mut guard = self.state.write();
873        let data = guard.get_or_create(&ctx.account);
874        self.require_server(data, &server_id)?;
875        let host_key_id = format!("hostkey-{}", hex17());
876        let key = format!("{server_id}/{host_key_id}");
877        let hk_arn = arn(ctx, &format!("host-key/{server_id}/{host_key_id}"));
878        let mut hk = Map::new();
879        hk.insert("Arn".into(), json!(hk_arn));
880        hk.insert("HostKeyId".into(), json!(host_key_id));
881        hk.insert(
882            "HostKeyFingerprint".into(),
883            json!(format!("SHA256:{}", hex17())),
884        );
885        hk.insert("Type".into(), json!(host_key_type(&host_key_body)));
886        hk.insert("DateImported".into(), json!(now_ts()));
887        copy_present(b, &mut hk, &["Description"]);
888        data.host_keys.insert(key, Value::Object(hk));
889        store_tags(data, &hk_arn, b);
890        ok(json!({ "ServerId": server_id, "HostKeyId": host_key_id }))
891    }
892
893    fn update_host_key(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
894        let server_id = req_str(b, "ServerId")?.to_string();
895        let host_key_id = req_str(b, "HostKeyId")?.to_string();
896        let description = req_str(b, "Description")?.to_string();
897        let mut guard = self.state.write();
898        let data = guard.get_or_create(&ctx.account);
899        let hk = data
900            .host_keys
901            .get_mut(&format!("{server_id}/{host_key_id}"))
902            .ok_or_else(|| not_found(&format!("Host key {host_key_id} does not exist.")))?;
903        hk.as_object_mut()
904            .unwrap()
905            .insert("Description".into(), json!(description));
906        ok(json!({ "ServerId": server_id, "HostKeyId": host_key_id }))
907    }
908
909    fn describe_host_key(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
910        let server_id = req_str(b, "ServerId")?.to_string();
911        let host_key_id = req_str(b, "HostKeyId")?.to_string();
912        let guard = self.state.read();
913        let data = guard
914            .get(&ctx.account)
915            .ok_or_else(|| not_found(&format!("Host key {host_key_id} does not exist.")))?;
916        let hk = data
917            .host_keys
918            .get(&format!("{server_id}/{host_key_id}"))
919            .ok_or_else(|| not_found(&format!("Host key {host_key_id} does not exist.")))?;
920        let mut out = hk.as_object().unwrap().clone();
921        let arn = out
922            .get("Arn")
923            .and_then(Value::as_str)
924            .unwrap_or("")
925            .to_string();
926        if let Some(tags) = tags_list(data, &arn) {
927            out.insert("Tags".into(), tags);
928        }
929        ok(json!({ "HostKey": Value::Object(out) }))
930    }
931
932    fn delete_host_key(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
933        let server_id = req_str(b, "ServerId")?.to_string();
934        let host_key_id = req_str(b, "HostKeyId")?.to_string();
935        let mut guard = self.state.write();
936        let data = guard.get_or_create(&ctx.account);
937        let hk = data
938            .host_keys
939            .remove(&format!("{server_id}/{host_key_id}"))
940            .ok_or_else(|| not_found(&format!("Host key {host_key_id} does not exist.")))?;
941        if let Some(arn) = hk.get("Arn").and_then(Value::as_str) {
942            data.tags.remove(arn);
943        }
944        ok(json!({}))
945    }
946
947    fn list_host_keys(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
948        let server_id = req_str(b, "ServerId")?.to_string();
949        let guard = self.state.read();
950        let data = guard
951            .get(&ctx.account)
952            .ok_or_else(|| not_found(&format!("Server {server_id} does not exist.")))?;
953        self.require_server(data, &server_id)?;
954        let prefix = format!("{server_id}/");
955        let rows: Vec<Value> = data
956            .host_keys
957            .iter()
958            .filter(|(k, _)| k.starts_with(&prefix))
959            .map(|(_, hk)| listed_host_key(hk))
960            .collect();
961        ok(list_response(
962            "HostKeys",
963            rows,
964            b,
965            &[("ServerId", json!(server_id))],
966        ))
967    }
968}
969
970fn listed_host_key(hk: &Value) -> Value {
971    let mut out = Map::new();
972    for (src, dst) in [
973        ("Arn", "Arn"),
974        ("HostKeyId", "HostKeyId"),
975        ("HostKeyFingerprint", "Fingerprint"),
976        ("Description", "Description"),
977        ("Type", "Type"),
978        ("DateImported", "DateImported"),
979    ] {
980        if let Some(v) = hk.get(src) {
981            out.insert(dst.to_string(), v.clone());
982        }
983    }
984    Value::Object(out)
985}
986
987// ===================== accesses =====================
988
989const ACCESS_FIELDS: &[&str] = &[
990    "HomeDirectory",
991    "HomeDirectoryMappings",
992    "HomeDirectoryType",
993    "Policy",
994    "PosixProfile",
995    "Role",
996];
997
998impl TransferService {
999    fn create_access(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1000        let server_id = req_str(b, "ServerId")?.to_string();
1001        let external_id = req_str(b, "ExternalId")?.to_string();
1002        req_str(b, "Role")?;
1003        let mut guard = self.state.write();
1004        let data = guard.get_or_create(&ctx.account);
1005        self.require_server(data, &server_id)?;
1006        let key = format!("{server_id}/{external_id}");
1007        if data.accesses.contains_key(&key) {
1008            return Err(resource_exists(&format!(
1009                "Access for {external_id} already exists."
1010            )));
1011        }
1012        let mut acc = Map::new();
1013        acc.insert("ExternalId".into(), json!(external_id));
1014        copy_present(b, &mut acc, ACCESS_FIELDS);
1015        data.accesses.insert(key, Value::Object(acc));
1016        ok(json!({ "ServerId": server_id, "ExternalId": external_id }))
1017    }
1018
1019    fn update_access(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1020        let server_id = req_str(b, "ServerId")?.to_string();
1021        let external_id = req_str(b, "ExternalId")?.to_string();
1022        let mut guard = self.state.write();
1023        let data = guard.get_or_create(&ctx.account);
1024        self.require_server(data, &server_id)?;
1025        let acc = data
1026            .accesses
1027            .get_mut(&format!("{server_id}/{external_id}"))
1028            .ok_or_else(|| not_found(&format!("Access {external_id} does not exist.")))?;
1029        copy_present(b, acc.as_object_mut().unwrap(), ACCESS_FIELDS);
1030        ok(json!({ "ServerId": server_id, "ExternalId": external_id }))
1031    }
1032
1033    fn describe_access(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1034        let server_id = req_str(b, "ServerId")?.to_string();
1035        let external_id = req_str(b, "ExternalId")?.to_string();
1036        let guard = self.state.read();
1037        let data = guard
1038            .get(&ctx.account)
1039            .ok_or_else(|| not_found(&format!("Access {external_id} does not exist.")))?;
1040        let acc = data
1041            .accesses
1042            .get(&format!("{server_id}/{external_id}"))
1043            .ok_or_else(|| not_found(&format!("Access {external_id} does not exist.")))?;
1044        ok(json!({ "ServerId": server_id, "Access": acc }))
1045    }
1046
1047    fn delete_access(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1048        let server_id = req_str(b, "ServerId")?.to_string();
1049        let external_id = req_str(b, "ExternalId")?.to_string();
1050        let mut guard = self.state.write();
1051        let data = guard.get_or_create(&ctx.account);
1052        data.accesses
1053            .remove(&format!("{server_id}/{external_id}"))
1054            .ok_or_else(|| not_found(&format!("Access {external_id} does not exist.")))?;
1055        ok(json!({}))
1056    }
1057
1058    fn list_accesses(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1059        let server_id = req_str(b, "ServerId")?.to_string();
1060        let guard = self.state.read();
1061        let data = guard
1062            .get(&ctx.account)
1063            .ok_or_else(|| not_found(&format!("Server {server_id} does not exist.")))?;
1064        self.require_server(data, &server_id)?;
1065        let prefix = format!("{server_id}/");
1066        let rows: Vec<Value> = data
1067            .accesses
1068            .iter()
1069            .filter(|(k, _)| k.starts_with(&prefix))
1070            .map(|(_, a)| {
1071                let mut out = Map::new();
1072                for f in ["HomeDirectory", "HomeDirectoryType", "Role", "ExternalId"] {
1073                    if let Some(v) = a.get(f) {
1074                        out.insert(f.to_string(), v.clone());
1075                    }
1076                }
1077                Value::Object(out)
1078            })
1079            .collect();
1080        ok(list_response(
1081            "Accesses",
1082            rows,
1083            b,
1084            &[("ServerId", json!(server_id))],
1085        ))
1086    }
1087}
1088
1089// ===================== workflows + executions =====================
1090
1091impl TransferService {
1092    fn create_workflow(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1093        if b.get("Steps").and_then(Value::as_array).is_none() {
1094            return Err(invalid_request("Steps is required."));
1095        }
1096        let workflow_id = format!("w-{}", hex17());
1097        let wf_arn = arn(ctx, &format!("workflow/{workflow_id}"));
1098        let mut wf = Map::new();
1099        wf.insert("Arn".into(), json!(wf_arn));
1100        wf.insert("WorkflowId".into(), json!(workflow_id));
1101        copy_present(b, &mut wf, &["Description", "Steps", "OnExceptionSteps"]);
1102        let mut guard = self.state.write();
1103        let data = guard.get_or_create(&ctx.account);
1104        data.workflows
1105            .insert(workflow_id.clone(), Value::Object(wf));
1106        store_tags(data, &wf_arn, b);
1107        ok(json!({ "WorkflowId": workflow_id }))
1108    }
1109
1110    fn describe_workflow(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1111        let workflow_id = req_str(b, "WorkflowId")?.to_string();
1112        let guard = self.state.read();
1113        let data = guard
1114            .get(&ctx.account)
1115            .ok_or_else(|| not_found(&format!("Workflow {workflow_id} does not exist.")))?;
1116        let wf = data
1117            .workflows
1118            .get(&workflow_id)
1119            .ok_or_else(|| not_found(&format!("Workflow {workflow_id} does not exist.")))?;
1120        let mut out = wf.as_object().unwrap().clone();
1121        let arn = out
1122            .get("Arn")
1123            .and_then(Value::as_str)
1124            .unwrap_or("")
1125            .to_string();
1126        if let Some(tags) = tags_list(data, &arn) {
1127            out.insert("Tags".into(), tags);
1128        }
1129        ok(json!({ "Workflow": Value::Object(out) }))
1130    }
1131
1132    fn delete_workflow(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1133        let workflow_id = req_str(b, "WorkflowId")?.to_string();
1134        let mut guard = self.state.write();
1135        let data = guard.get_or_create(&ctx.account);
1136        let wf = data
1137            .workflows
1138            .remove(&workflow_id)
1139            .ok_or_else(|| not_found(&format!("Workflow {workflow_id} does not exist.")))?;
1140        if let Some(arn) = wf.get("Arn").and_then(Value::as_str) {
1141            data.tags.remove(arn);
1142        }
1143        let prefix = format!("{workflow_id}/");
1144        data.executions.retain(|k, _| !k.starts_with(&prefix));
1145        ok(json!({}))
1146    }
1147
1148    fn list_workflows(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1149        let guard = self.state.read();
1150        let rows: Vec<Value> = guard
1151            .get(&ctx.account)
1152            .map(|d| {
1153                d.workflows
1154                    .values()
1155                    .map(|w| {
1156                        let mut out = Map::new();
1157                        for f in ["WorkflowId", "Description", "Arn"] {
1158                            if let Some(v) = w.get(f) {
1159                                out.insert(f.to_string(), v.clone());
1160                            }
1161                        }
1162                        Value::Object(out)
1163                    })
1164                    .collect()
1165            })
1166            .unwrap_or_default();
1167        ok(list_response("Workflows", rows, b, &[]))
1168    }
1169
1170    fn send_workflow_step_state(
1171        &self,
1172        ctx: &Ctx,
1173        b: &Value,
1174    ) -> Result<AwsResponse, AwsServiceError> {
1175        let workflow_id = req_str(b, "WorkflowId")?.to_string();
1176        let execution_id = req_str(b, "ExecutionId")?.to_string();
1177        req_str(b, "Token")?;
1178        req_str(b, "Status")?;
1179        let guard = self.state.read();
1180        let data = guard
1181            .get(&ctx.account)
1182            .ok_or_else(|| not_found(&format!("Execution {execution_id} does not exist.")))?;
1183        if !data
1184            .executions
1185            .contains_key(&format!("{workflow_id}/{execution_id}"))
1186        {
1187            return Err(not_found(&format!(
1188                "Execution {execution_id} does not exist."
1189            )));
1190        }
1191        ok(json!({}))
1192    }
1193
1194    fn describe_execution(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1195        let workflow_id = req_str(b, "WorkflowId")?.to_string();
1196        let execution_id = req_str(b, "ExecutionId")?.to_string();
1197        let guard = self.state.read();
1198        let data = guard
1199            .get(&ctx.account)
1200            .ok_or_else(|| not_found(&format!("Execution {execution_id} does not exist.")))?;
1201        let exec = data
1202            .executions
1203            .get(&format!("{workflow_id}/{execution_id}"))
1204            .ok_or_else(|| not_found(&format!("Execution {execution_id} does not exist.")))?;
1205        ok(json!({ "WorkflowId": workflow_id, "Execution": exec }))
1206    }
1207
1208    fn list_executions(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1209        let workflow_id = req_str(b, "WorkflowId")?.to_string();
1210        let guard = self.state.read();
1211        let data = guard
1212            .get(&ctx.account)
1213            .ok_or_else(|| not_found(&format!("Workflow {workflow_id} does not exist.")))?;
1214        if !data.workflows.contains_key(&workflow_id) {
1215            return Err(not_found(&format!(
1216                "Workflow {workflow_id} does not exist."
1217            )));
1218        }
1219        let prefix = format!("{workflow_id}/");
1220        let rows: Vec<Value> = data
1221            .executions
1222            .iter()
1223            .filter(|(k, _)| k.starts_with(&prefix))
1224            .map(|(_, e)| {
1225                let mut out = Map::new();
1226                for f in [
1227                    "ExecutionId",
1228                    "InitialFileLocation",
1229                    "ServiceMetadata",
1230                    "Status",
1231                ] {
1232                    if let Some(v) = e.get(f) {
1233                        out.insert(f.to_string(), v.clone());
1234                    }
1235                }
1236                Value::Object(out)
1237            })
1238            .collect();
1239        ok(list_response(
1240            "Executions",
1241            rows,
1242            b,
1243            &[("WorkflowId", json!(workflow_id))],
1244        ))
1245    }
1246}
1247
1248// ===================== agreements =====================
1249
1250const AGREEMENT_FIELDS: &[&str] = &[
1251    "Description",
1252    "Status",
1253    "LocalProfileId",
1254    "PartnerProfileId",
1255    "BaseDirectory",
1256    "AccessRole",
1257    "PreserveFilename",
1258    "EnforceMessageSigning",
1259    "CustomDirectories",
1260];
1261
1262impl TransferService {
1263    fn create_agreement(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1264        let server_id = req_str(b, "ServerId")?.to_string();
1265        req_str(b, "LocalProfileId")?;
1266        req_str(b, "PartnerProfileId")?;
1267        req_str(b, "AccessRole")?;
1268        let mut guard = self.state.write();
1269        let data = guard.get_or_create(&ctx.account);
1270        self.require_server(data, &server_id)?;
1271        let agreement_id = format!("a-{}", hex17());
1272        let ag_arn = arn(ctx, &format!("agreement/{agreement_id}"));
1273        let mut ag = Map::new();
1274        ag.insert("Arn".into(), json!(ag_arn));
1275        ag.insert("AgreementId".into(), json!(agreement_id));
1276        ag.insert("ServerId".into(), json!(server_id));
1277        ag.insert(
1278            "Status".into(),
1279            b.get("Status").cloned().unwrap_or(json!("ACTIVE")),
1280        );
1281        copy_present(b, &mut ag, AGREEMENT_FIELDS);
1282        data.agreements
1283            .insert(format!("{server_id}/{agreement_id}"), Value::Object(ag));
1284        store_tags(data, &ag_arn, b);
1285        ok(json!({ "AgreementId": agreement_id }))
1286    }
1287
1288    fn update_agreement(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1289        let agreement_id = req_str(b, "AgreementId")?.to_string();
1290        let server_id = req_str(b, "ServerId")?.to_string();
1291        let mut guard = self.state.write();
1292        let data = guard.get_or_create(&ctx.account);
1293        let ag = data
1294            .agreements
1295            .get_mut(&format!("{server_id}/{agreement_id}"))
1296            .ok_or_else(|| not_found(&format!("Agreement {agreement_id} does not exist.")))?;
1297        copy_present(b, ag.as_object_mut().unwrap(), AGREEMENT_FIELDS);
1298        ok(json!({ "AgreementId": agreement_id }))
1299    }
1300
1301    fn describe_agreement(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1302        let agreement_id = req_str(b, "AgreementId")?.to_string();
1303        let server_id = req_str(b, "ServerId")?.to_string();
1304        let guard = self.state.read();
1305        let data = guard
1306            .get(&ctx.account)
1307            .ok_or_else(|| not_found(&format!("Agreement {agreement_id} does not exist.")))?;
1308        let ag = data
1309            .agreements
1310            .get(&format!("{server_id}/{agreement_id}"))
1311            .ok_or_else(|| not_found(&format!("Agreement {agreement_id} does not exist.")))?;
1312        let mut out = ag.as_object().unwrap().clone();
1313        let arn = out
1314            .get("Arn")
1315            .and_then(Value::as_str)
1316            .unwrap_or("")
1317            .to_string();
1318        if let Some(tags) = tags_list(data, &arn) {
1319            out.insert("Tags".into(), tags);
1320        }
1321        ok(json!({ "Agreement": Value::Object(out) }))
1322    }
1323
1324    fn delete_agreement(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1325        let agreement_id = req_str(b, "AgreementId")?.to_string();
1326        let server_id = req_str(b, "ServerId")?.to_string();
1327        let mut guard = self.state.write();
1328        let data = guard.get_or_create(&ctx.account);
1329        let ag = data
1330            .agreements
1331            .remove(&format!("{server_id}/{agreement_id}"))
1332            .ok_or_else(|| not_found(&format!("Agreement {agreement_id} does not exist.")))?;
1333        if let Some(arn) = ag.get("Arn").and_then(Value::as_str) {
1334            data.tags.remove(arn);
1335        }
1336        ok(json!({}))
1337    }
1338
1339    fn list_agreements(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1340        let server_id = req_str(b, "ServerId")?.to_string();
1341        let guard = self.state.read();
1342        let data = guard
1343            .get(&ctx.account)
1344            .ok_or_else(|| not_found(&format!("Server {server_id} does not exist.")))?;
1345        self.require_server(data, &server_id)?;
1346        let prefix = format!("{server_id}/");
1347        let rows: Vec<Value> = data
1348            .agreements
1349            .iter()
1350            .filter(|(k, _)| k.starts_with(&prefix))
1351            .map(|(_, a)| {
1352                let mut out = Map::new();
1353                for f in [
1354                    "Arn",
1355                    "AgreementId",
1356                    "Description",
1357                    "Status",
1358                    "ServerId",
1359                    "LocalProfileId",
1360                    "PartnerProfileId",
1361                ] {
1362                    if let Some(v) = a.get(f) {
1363                        out.insert(f.to_string(), v.clone());
1364                    }
1365                }
1366                Value::Object(out)
1367            })
1368            .collect();
1369        ok(list_response("Agreements", rows, b, &[]))
1370    }
1371}
1372
1373// ===================== connectors =====================
1374
1375const CONNECTOR_FIELDS: &[&str] = &[
1376    "Url",
1377    "As2Config",
1378    "AccessRole",
1379    "LoggingRole",
1380    "SftpConfig",
1381    "SecurityPolicyName",
1382    "IpAddressType",
1383];
1384
1385impl TransferService {
1386    fn require_connector<'a>(
1387        &self,
1388        data: &'a TransferData,
1389        connector_id: &str,
1390    ) -> Result<&'a Value, AwsServiceError> {
1391        data.connectors
1392            .get(connector_id)
1393            .ok_or_else(|| not_found(&format!("Connector {connector_id} does not exist.")))
1394    }
1395
1396    fn create_connector(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1397        req_str(b, "AccessRole")?;
1398        let connector_id = format!("c-{}", hex17());
1399        let c_arn = arn(ctx, &format!("connector/{connector_id}"));
1400        let mut c = Map::new();
1401        c.insert("Arn".into(), json!(c_arn));
1402        c.insert("ConnectorId".into(), json!(connector_id));
1403        c.insert("EgressType".into(), json!("SERVICE_MANAGED"));
1404        c.insert("Status".into(), json!("ACTIVE"));
1405        c.insert(
1406            "ServiceManagedEgressIpAddresses".into(),
1407            json!(["3.219.0.10", "52.4.0.20"]),
1408        );
1409        copy_present(b, &mut c, CONNECTOR_FIELDS);
1410        let mut guard = self.state.write();
1411        let data = guard.get_or_create(&ctx.account);
1412        data.connectors
1413            .insert(connector_id.clone(), Value::Object(c));
1414        store_tags(data, &c_arn, b);
1415        ok(json!({ "ConnectorId": connector_id }))
1416    }
1417
1418    fn update_connector(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1419        let connector_id = req_str(b, "ConnectorId")?.to_string();
1420        let mut guard = self.state.write();
1421        let data = guard.get_or_create(&ctx.account);
1422        let c = data
1423            .connectors
1424            .get_mut(&connector_id)
1425            .ok_or_else(|| not_found(&format!("Connector {connector_id} does not exist.")))?;
1426        copy_present(b, c.as_object_mut().unwrap(), CONNECTOR_FIELDS);
1427        ok(json!({ "ConnectorId": connector_id }))
1428    }
1429
1430    fn describe_connector(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1431        let connector_id = req_str(b, "ConnectorId")?.to_string();
1432        let guard = self.state.read();
1433        let data = guard
1434            .get(&ctx.account)
1435            .ok_or_else(|| not_found(&format!("Connector {connector_id} does not exist.")))?;
1436        let c = self.require_connector(data, &connector_id)?;
1437        let mut out = c.as_object().unwrap().clone();
1438        let arn = out
1439            .get("Arn")
1440            .and_then(Value::as_str)
1441            .unwrap_or("")
1442            .to_string();
1443        if let Some(tags) = tags_list(data, &arn) {
1444            out.insert("Tags".into(), tags);
1445        }
1446        ok(json!({ "Connector": Value::Object(out) }))
1447    }
1448
1449    fn delete_connector(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1450        let connector_id = req_str(b, "ConnectorId")?.to_string();
1451        let mut guard = self.state.write();
1452        let data = guard.get_or_create(&ctx.account);
1453        let c = data
1454            .connectors
1455            .remove(&connector_id)
1456            .ok_or_else(|| not_found(&format!("Connector {connector_id} does not exist.")))?;
1457        if let Some(arn) = c.get("Arn").and_then(Value::as_str) {
1458            data.tags.remove(arn);
1459        }
1460        let prefix = format!("{connector_id}/");
1461        data.file_transfers.retain(|k, _| !k.starts_with(&prefix));
1462        ok(json!({}))
1463    }
1464
1465    fn list_connectors(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1466        let guard = self.state.read();
1467        let rows: Vec<Value> = guard
1468            .get(&ctx.account)
1469            .map(|d| {
1470                d.connectors
1471                    .values()
1472                    .map(|c| {
1473                        let mut out = Map::new();
1474                        for f in ["Arn", "ConnectorId", "Url"] {
1475                            if let Some(v) = c.get(f) {
1476                                out.insert(f.to_string(), v.clone());
1477                            }
1478                        }
1479                        Value::Object(out)
1480                    })
1481                    .collect()
1482            })
1483            .unwrap_or_default();
1484        ok(list_response("Connectors", rows, b, &[]))
1485    }
1486
1487    fn test_connection(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1488        let connector_id = req_str(b, "ConnectorId")?.to_string();
1489        let guard = self.state.read();
1490        let data = guard
1491            .get(&ctx.account)
1492            .ok_or_else(|| not_found(&format!("Connector {connector_id} does not exist.")))?;
1493        self.require_connector(data, &connector_id)?;
1494        ok(json!({
1495            "ConnectorId": connector_id,
1496            "Status": "OK",
1497            "StatusMessage": "Connection succeeded"
1498        }))
1499    }
1500
1501    fn start_directory_listing(
1502        &self,
1503        ctx: &Ctx,
1504        b: &Value,
1505    ) -> Result<AwsResponse, AwsServiceError> {
1506        let connector_id = req_str(b, "ConnectorId")?.to_string();
1507        req_str(b, "RemoteDirectoryPath")?;
1508        req_str(b, "OutputDirectoryPath")?;
1509        let guard = self.state.read();
1510        let data = guard
1511            .get(&ctx.account)
1512            .ok_or_else(|| not_found(&format!("Connector {connector_id} does not exist.")))?;
1513        self.require_connector(data, &connector_id)?;
1514        let listing_id = Uuid::new_v4().to_string();
1515        ok(json!({
1516            "ListingId": listing_id,
1517            "OutputFileName": format!("{connector_id}-{listing_id}.json")
1518        }))
1519    }
1520
1521    fn start_file_transfer(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1522        let connector_id = req_str(b, "ConnectorId")?.to_string();
1523        let mut guard = self.state.write();
1524        let data = guard.get_or_create(&ctx.account);
1525        self.require_connector(data, &connector_id)?;
1526        let transfer_id = Uuid::new_v4().to_string();
1527        // Record a completed result per requested path so
1528        // ListFileTransferResults reflects the transfer.
1529        let mut results = Vec::new();
1530        for field in ["SendFilePaths", "RetrieveFilePaths"] {
1531            if let Some(paths) = b.get(field).and_then(Value::as_array) {
1532                for p in paths.iter().filter_map(Value::as_str) {
1533                    results.push(json!({ "FilePath": p, "StatusCode": "COMPLETED" }));
1534                }
1535            }
1536        }
1537        data.file_transfers.insert(
1538            format!("{connector_id}/{transfer_id}"),
1539            json!({ "Results": results }),
1540        );
1541        ok(json!({ "TransferId": transfer_id }))
1542    }
1543
1544    fn start_remote_delete(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1545        let connector_id = req_str(b, "ConnectorId")?.to_string();
1546        req_str(b, "DeletePath")?;
1547        let guard = self.state.read();
1548        let data = guard
1549            .get(&ctx.account)
1550            .ok_or_else(|| not_found(&format!("Connector {connector_id} does not exist.")))?;
1551        self.require_connector(data, &connector_id)?;
1552        ok(json!({ "DeleteId": Uuid::new_v4().to_string() }))
1553    }
1554
1555    fn start_remote_move(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1556        let connector_id = req_str(b, "ConnectorId")?.to_string();
1557        req_str(b, "SourcePath")?;
1558        req_str(b, "TargetPath")?;
1559        let guard = self.state.read();
1560        let data = guard
1561            .get(&ctx.account)
1562            .ok_or_else(|| not_found(&format!("Connector {connector_id} does not exist.")))?;
1563        self.require_connector(data, &connector_id)?;
1564        ok(json!({ "MoveId": Uuid::new_v4().to_string() }))
1565    }
1566
1567    fn list_file_transfer_results(
1568        &self,
1569        ctx: &Ctx,
1570        b: &Value,
1571    ) -> Result<AwsResponse, AwsServiceError> {
1572        let connector_id = req_str(b, "ConnectorId")?.to_string();
1573        let transfer_id = req_str(b, "TransferId")?.to_string();
1574        let guard = self.state.read();
1575        let data = guard
1576            .get(&ctx.account)
1577            .ok_or_else(|| not_found(&format!("Connector {connector_id} does not exist.")))?;
1578        self.require_connector(data, &connector_id)?;
1579        let rows: Vec<Value> = data
1580            .file_transfers
1581            .get(&format!("{connector_id}/{transfer_id}"))
1582            .and_then(|t| t.get("Results"))
1583            .and_then(Value::as_array)
1584            .cloned()
1585            .unwrap_or_default();
1586        ok(list_response("FileTransferResults", rows, b, &[]))
1587    }
1588}
1589
1590// ===================== profiles =====================
1591
1592impl TransferService {
1593    fn create_profile(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1594        req_str(b, "As2Id")?;
1595        req_str(b, "ProfileType")?;
1596        let profile_id = format!("p-{}", hex17());
1597        let p_arn = arn(ctx, &format!("profile/{profile_id}"));
1598        let mut p = Map::new();
1599        p.insert("Arn".into(), json!(p_arn));
1600        p.insert("ProfileId".into(), json!(profile_id));
1601        copy_present(b, &mut p, &["As2Id", "ProfileType", "CertificateIds"]);
1602        let mut guard = self.state.write();
1603        let data = guard.get_or_create(&ctx.account);
1604        data.profiles.insert(profile_id.clone(), Value::Object(p));
1605        store_tags(data, &p_arn, b);
1606        ok(json!({ "ProfileId": profile_id }))
1607    }
1608
1609    fn update_profile(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1610        let profile_id = req_str(b, "ProfileId")?.to_string();
1611        let mut guard = self.state.write();
1612        let data = guard.get_or_create(&ctx.account);
1613        let p = data
1614            .profiles
1615            .get_mut(&profile_id)
1616            .ok_or_else(|| not_found(&format!("Profile {profile_id} does not exist.")))?;
1617        copy_present(b, p.as_object_mut().unwrap(), &["CertificateIds"]);
1618        ok(json!({ "ProfileId": profile_id }))
1619    }
1620
1621    fn describe_profile(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1622        let profile_id = req_str(b, "ProfileId")?.to_string();
1623        let guard = self.state.read();
1624        let data = guard
1625            .get(&ctx.account)
1626            .ok_or_else(|| not_found(&format!("Profile {profile_id} does not exist.")))?;
1627        let p = data
1628            .profiles
1629            .get(&profile_id)
1630            .ok_or_else(|| not_found(&format!("Profile {profile_id} does not exist.")))?;
1631        let mut out = p.as_object().unwrap().clone();
1632        let arn = out
1633            .get("Arn")
1634            .and_then(Value::as_str)
1635            .unwrap_or("")
1636            .to_string();
1637        if let Some(tags) = tags_list(data, &arn) {
1638            out.insert("Tags".into(), tags);
1639        }
1640        ok(json!({ "Profile": Value::Object(out) }))
1641    }
1642
1643    fn delete_profile(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1644        let profile_id = req_str(b, "ProfileId")?.to_string();
1645        let mut guard = self.state.write();
1646        let data = guard.get_or_create(&ctx.account);
1647        let p = data
1648            .profiles
1649            .remove(&profile_id)
1650            .ok_or_else(|| not_found(&format!("Profile {profile_id} does not exist.")))?;
1651        if let Some(arn) = p.get("Arn").and_then(Value::as_str) {
1652            data.tags.remove(arn);
1653        }
1654        ok(json!({}))
1655    }
1656
1657    fn list_profiles(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1658        let filter = opt_str(b, "ProfileType").map(String::from);
1659        let guard = self.state.read();
1660        let rows: Vec<Value> = guard
1661            .get(&ctx.account)
1662            .map(|d| {
1663                d.profiles
1664                    .values()
1665                    .filter(|p| {
1666                        filter
1667                            .as_deref()
1668                            .is_none_or(|f| p.get("ProfileType").and_then(Value::as_str) == Some(f))
1669                    })
1670                    .map(|p| {
1671                        let mut out = Map::new();
1672                        for f in ["Arn", "ProfileId", "As2Id", "ProfileType"] {
1673                            if let Some(v) = p.get(f) {
1674                                out.insert(f.to_string(), v.clone());
1675                            }
1676                        }
1677                        Value::Object(out)
1678                    })
1679                    .collect()
1680            })
1681            .unwrap_or_default();
1682        ok(list_response("Profiles", rows, b, &[]))
1683    }
1684}
1685
1686// ===================== certificates =====================
1687
1688/// Parse a PEM `Certificate` body and extract the computed
1689/// `Serial`/`NotBeforeDate`/`NotAfterDate` fields AWS returns on a
1690/// `DescribedCertificate`. Returns `None` on any parse failure (never panics).
1691fn parse_certificate_fields(pem: &str) -> Option<(String, f64, f64)> {
1692    use x509_cert::der::DecodePem;
1693    use x509_cert::Certificate;
1694
1695    let cert = Certificate::from_pem(pem.as_bytes()).ok()?;
1696    let tbs = &cert.tbs_certificate;
1697    // Serial number as a lowercase hex string, matching AWS's rendering.
1698    let serial = tbs
1699        .serial_number
1700        .as_bytes()
1701        .iter()
1702        .map(|byte| format!("{byte:02x}"))
1703        .collect::<String>();
1704    let not_before = tbs.validity.not_before.to_unix_duration().as_secs() as f64;
1705    let not_after = tbs.validity.not_after.to_unix_duration().as_secs() as f64;
1706    Some((serial, not_before, not_after))
1707}
1708
1709impl TransferService {
1710    fn import_certificate(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1711        req_str(b, "Usage")?;
1712        let certificate_pem = req_str(b, "Certificate")?.to_string();
1713        let certificate_id = format!("cert-{}", hex17());
1714        let c_arn = arn(ctx, &format!("certificate/{certificate_id}"));
1715        let mut c = Map::new();
1716        c.insert("Arn".into(), json!(c_arn));
1717        c.insert("CertificateId".into(), json!(certificate_id));
1718        c.insert("Status".into(), json!("ACTIVE"));
1719        c.insert("Type".into(), json!("CERTIFICATE"));
1720        // Populate the computed Serial/NotBeforeDate/NotAfterDate from the PEM.
1721        // A body that does not parse (e.g. a placeholder) simply leaves them
1722        // unset, exactly as an unparsable cert would omit them.
1723        if let Some((serial, not_before, not_after)) = parse_certificate_fields(&certificate_pem) {
1724            c.insert("Serial".into(), json!(serial));
1725            c.insert("NotBeforeDate".into(), json!(not_before));
1726            c.insert("NotAfterDate".into(), json!(not_after));
1727        }
1728        copy_present(
1729            b,
1730            &mut c,
1731            &[
1732                "Usage",
1733                "Certificate",
1734                "CertificateChain",
1735                "ActiveDate",
1736                "InactiveDate",
1737                "Description",
1738            ],
1739        );
1740        let mut guard = self.state.write();
1741        let data = guard.get_or_create(&ctx.account);
1742        data.certificates
1743            .insert(certificate_id.clone(), Value::Object(c));
1744        store_tags(data, &c_arn, b);
1745        ok(json!({ "CertificateId": certificate_id }))
1746    }
1747
1748    fn update_certificate(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1749        let certificate_id = req_str(b, "CertificateId")?.to_string();
1750        let mut guard = self.state.write();
1751        let data = guard.get_or_create(&ctx.account);
1752        let c = data
1753            .certificates
1754            .get_mut(&certificate_id)
1755            .ok_or_else(|| not_found(&format!("Certificate {certificate_id} does not exist.")))?;
1756        copy_present(
1757            b,
1758            c.as_object_mut().unwrap(),
1759            &["ActiveDate", "InactiveDate", "Description"],
1760        );
1761        ok(json!({ "CertificateId": certificate_id }))
1762    }
1763
1764    fn describe_certificate(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1765        let certificate_id = req_str(b, "CertificateId")?.to_string();
1766        let guard = self.state.read();
1767        let data = guard
1768            .get(&ctx.account)
1769            .ok_or_else(|| not_found(&format!("Certificate {certificate_id} does not exist.")))?;
1770        let c = data
1771            .certificates
1772            .get(&certificate_id)
1773            .ok_or_else(|| not_found(&format!("Certificate {certificate_id} does not exist.")))?;
1774        let mut out = c.as_object().unwrap().clone();
1775        let arn = out
1776            .get("Arn")
1777            .and_then(Value::as_str)
1778            .unwrap_or("")
1779            .to_string();
1780        if let Some(tags) = tags_list(data, &arn) {
1781            out.insert("Tags".into(), tags);
1782        }
1783        ok(json!({ "Certificate": Value::Object(out) }))
1784    }
1785
1786    fn delete_certificate(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1787        let certificate_id = req_str(b, "CertificateId")?.to_string();
1788        let mut guard = self.state.write();
1789        let data = guard.get_or_create(&ctx.account);
1790        let c = data
1791            .certificates
1792            .remove(&certificate_id)
1793            .ok_or_else(|| not_found(&format!("Certificate {certificate_id} does not exist.")))?;
1794        if let Some(arn) = c.get("Arn").and_then(Value::as_str) {
1795            data.tags.remove(arn);
1796        }
1797        ok(json!({}))
1798    }
1799
1800    fn list_certificates(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1801        let guard = self.state.read();
1802        let rows: Vec<Value> = guard
1803            .get(&ctx.account)
1804            .map(|d| {
1805                d.certificates
1806                    .values()
1807                    .map(|c| {
1808                        let mut out = Map::new();
1809                        for f in [
1810                            "Arn",
1811                            "CertificateId",
1812                            "Usage",
1813                            "Status",
1814                            "ActiveDate",
1815                            "InactiveDate",
1816                            "Type",
1817                            "Description",
1818                        ] {
1819                            if let Some(v) = c.get(f) {
1820                                out.insert(f.to_string(), v.clone());
1821                            }
1822                        }
1823                        Value::Object(out)
1824                    })
1825                    .collect()
1826            })
1827            .unwrap_or_default();
1828        ok(list_response("Certificates", rows, b, &[]))
1829    }
1830}
1831
1832// ===================== security policies =====================
1833
1834/// The real AWS Transfer Family managed security policy names.
1835const SECURITY_POLICIES: &[&str] = &[
1836    "TransferSecurityPolicy-2018-11",
1837    "TransferSecurityPolicy-2020-06",
1838    "TransferSecurityPolicy-2022-03",
1839    "TransferSecurityPolicy-2023-05",
1840    "TransferSecurityPolicy-2024-01",
1841    "TransferSecurityPolicy-FIPS-2020-06",
1842    "TransferSecurityPolicy-FIPS-2023-05",
1843    "TransferSecurityPolicy-FIPS-2024-01",
1844    "TransferSecurityPolicy-FIPS-2024-05",
1845    "TransferSecurityPolicy-PQ-SSH-Experimental-2023-04",
1846    "TransferSecurityPolicy-PQ-SSH-FIPS-Experimental-2023-04",
1847    "TransferSecurityPolicy-Restricted-2018-11",
1848    "TransferSecurityPolicy-Restricted-2020-06",
1849];
1850
1851impl TransferService {
1852    fn describe_security_policy(&self, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1853        let name = req_str(b, "SecurityPolicyName")?;
1854        if !SECURITY_POLICIES.contains(&name) {
1855            return Err(not_found(&format!(
1856                "Security policy {name} does not exist."
1857            )));
1858        }
1859        let fips = name.contains("FIPS");
1860        let policy_type = if name.contains("Connector") {
1861            "CONNECTOR"
1862        } else {
1863            "SERVER"
1864        };
1865        ok(json!({
1866            "SecurityPolicy": {
1867                "SecurityPolicyName": name,
1868                "Fips": fips,
1869                "Type": policy_type,
1870                "SshCiphers": ["aes256-gcm@openssh.com", "aes128-gcm@openssh.com"],
1871                "SshKexs": ["ecdh-sha2-nistp256", "diffie-hellman-group-exchange-sha256"],
1872                "SshMacs": ["hmac-sha2-256", "hmac-sha2-512"],
1873                "TlsCiphers": ["TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384"]
1874            }
1875        }))
1876    }
1877
1878    fn list_security_policies(&self, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1879        let rows: Vec<Value> = SECURITY_POLICIES.iter().map(|p| json!(p)).collect();
1880        ok(list_response("SecurityPolicyNames", rows, b, &[]))
1881    }
1882}
1883
1884// ===================== web apps =====================
1885
1886const WEB_APP_FIELDS: &[&str] = &["AccessEndpoint", "WebAppUnits", "WebAppEndpointPolicy"];
1887
1888impl TransferService {
1889    fn create_web_app(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1890        if b.get("IdentityProviderDetails").is_none() {
1891            return Err(invalid_request("IdentityProviderDetails is required."));
1892        }
1893        let web_app_id = format!("webapp-{}", hex17());
1894        let w_arn = arn(ctx, &format!("webapp/{web_app_id}"));
1895        let mut w = Map::new();
1896        w.insert("Arn".into(), json!(w_arn));
1897        w.insert("WebAppId".into(), json!(web_app_id));
1898        w.insert(
1899            "WebAppEndpoint".into(),
1900            json!(format!(
1901                "{web_app_id}.transfer.{}.amazonaws.com",
1902                ctx.region
1903            )),
1904        );
1905        w.insert("EndpointType".into(), json!("STANDARD"));
1906        if let Some(idp) = b.get("IdentityProviderDetails") {
1907            w.insert("DescribedIdentityProviderDetails".into(), idp.clone());
1908        }
1909        copy_present(b, &mut w, WEB_APP_FIELDS);
1910        let mut guard = self.state.write();
1911        let data = guard.get_or_create(&ctx.account);
1912        data.web_apps.insert(web_app_id.clone(), Value::Object(w));
1913        store_tags(data, &w_arn, b);
1914        ok(json!({ "WebAppId": web_app_id }))
1915    }
1916
1917    fn update_web_app(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1918        let web_app_id = req_str(b, "WebAppId")?.to_string();
1919        let mut guard = self.state.write();
1920        let data = guard.get_or_create(&ctx.account);
1921        let w = data
1922            .web_apps
1923            .get_mut(&web_app_id)
1924            .ok_or_else(|| not_found(&format!("Web app {web_app_id} does not exist.")))?;
1925        let obj = w.as_object_mut().unwrap();
1926        if let Some(idp) = b.get("IdentityProviderDetails") {
1927            obj.insert("DescribedIdentityProviderDetails".into(), idp.clone());
1928        }
1929        copy_present(b, obj, WEB_APP_FIELDS);
1930        ok(json!({ "WebAppId": web_app_id }))
1931    }
1932
1933    fn describe_web_app(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1934        let web_app_id = req_str(b, "WebAppId")?.to_string();
1935        let guard = self.state.read();
1936        let data = guard
1937            .get(&ctx.account)
1938            .ok_or_else(|| not_found(&format!("Web app {web_app_id} does not exist.")))?;
1939        let w = data
1940            .web_apps
1941            .get(&web_app_id)
1942            .ok_or_else(|| not_found(&format!("Web app {web_app_id} does not exist.")))?;
1943        let mut out = w.as_object().unwrap().clone();
1944        let arn = out
1945            .get("Arn")
1946            .and_then(Value::as_str)
1947            .unwrap_or("")
1948            .to_string();
1949        if let Some(tags) = tags_list(data, &arn) {
1950            out.insert("Tags".into(), tags);
1951        }
1952        ok(json!({ "WebApp": Value::Object(out) }))
1953    }
1954
1955    fn delete_web_app(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1956        let web_app_id = req_str(b, "WebAppId")?.to_string();
1957        let mut guard = self.state.write();
1958        let data = guard.get_or_create(&ctx.account);
1959        let w = data
1960            .web_apps
1961            .remove(&web_app_id)
1962            .ok_or_else(|| not_found(&format!("Web app {web_app_id} does not exist.")))?;
1963        data.web_app_customizations.remove(&web_app_id);
1964        if let Some(arn) = w.get("Arn").and_then(Value::as_str) {
1965            data.tags.remove(arn);
1966        }
1967        ok(json!({}))
1968    }
1969
1970    fn list_web_apps(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1971        let guard = self.state.read();
1972        let rows: Vec<Value> = guard
1973            .get(&ctx.account)
1974            .map(|d| {
1975                d.web_apps
1976                    .values()
1977                    .map(|w| {
1978                        let mut out = Map::new();
1979                        for f in [
1980                            "Arn",
1981                            "WebAppId",
1982                            "AccessEndpoint",
1983                            "WebAppEndpoint",
1984                            "EndpointType",
1985                        ] {
1986                            if let Some(v) = w.get(f) {
1987                                out.insert(f.to_string(), v.clone());
1988                            }
1989                        }
1990                        Value::Object(out)
1991                    })
1992                    .collect()
1993            })
1994            .unwrap_or_default();
1995        ok(list_response("WebApps", rows, b, &[]))
1996    }
1997
1998    fn update_web_app_customization(
1999        &self,
2000        ctx: &Ctx,
2001        b: &Value,
2002    ) -> Result<AwsResponse, AwsServiceError> {
2003        let web_app_id = req_str(b, "WebAppId")?.to_string();
2004        let mut guard = self.state.write();
2005        let data = guard.get_or_create(&ctx.account);
2006        let w = data
2007            .web_apps
2008            .get(&web_app_id)
2009            .ok_or_else(|| not_found(&format!("Web app {web_app_id} does not exist.")))?;
2010        let arn = w
2011            .get("Arn")
2012            .and_then(Value::as_str)
2013            .unwrap_or("")
2014            .replace("webapp/", "webapp-customization/");
2015        let mut cust = Map::new();
2016        cust.insert("Arn".into(), json!(arn));
2017        cust.insert("WebAppId".into(), json!(web_app_id));
2018        copy_present(b, &mut cust, &["Title", "LogoFile", "FaviconFile"]);
2019        data.web_app_customizations
2020            .insert(web_app_id.clone(), Value::Object(cust));
2021        ok(json!({ "WebAppId": web_app_id }))
2022    }
2023
2024    fn describe_web_app_customization(
2025        &self,
2026        ctx: &Ctx,
2027        b: &Value,
2028    ) -> Result<AwsResponse, AwsServiceError> {
2029        let web_app_id = req_str(b, "WebAppId")?.to_string();
2030        let guard = self.state.read();
2031        let data = guard
2032            .get(&ctx.account)
2033            .ok_or_else(|| not_found(&format!("Web app {web_app_id} does not exist.")))?;
2034        if let Some(cust) = data.web_app_customizations.get(&web_app_id) {
2035            return ok(json!({ "WebAppCustomization": cust }));
2036        }
2037        // A web app with no explicit customization still yields the default
2038        // (empty) customization record.
2039        let w = data
2040            .web_apps
2041            .get(&web_app_id)
2042            .ok_or_else(|| not_found(&format!("Web app {web_app_id} does not exist.")))?;
2043        let arn = w
2044            .get("Arn")
2045            .and_then(Value::as_str)
2046            .unwrap_or("")
2047            .replace("webapp/", "webapp-customization/");
2048        ok(json!({
2049            "WebAppCustomization": { "Arn": arn, "WebAppId": web_app_id }
2050        }))
2051    }
2052
2053    fn delete_web_app_customization(
2054        &self,
2055        ctx: &Ctx,
2056        b: &Value,
2057    ) -> Result<AwsResponse, AwsServiceError> {
2058        let web_app_id = req_str(b, "WebAppId")?.to_string();
2059        let mut guard = self.state.write();
2060        let data = guard.get_or_create(&ctx.account);
2061        if !data.web_apps.contains_key(&web_app_id) {
2062            return Err(not_found(&format!("Web app {web_app_id} does not exist.")));
2063        }
2064        data.web_app_customizations.remove(&web_app_id);
2065        ok(json!({}))
2066    }
2067}
2068
2069// ===================== identity provider =====================
2070
2071impl TransferService {
2072    fn test_identity_provider(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
2073        let server_id = req_str(b, "ServerId")?.to_string();
2074        req_str(b, "UserName")?;
2075        let guard = self.state.read();
2076        let data = guard
2077            .get(&ctx.account)
2078            .ok_or_else(|| not_found(&format!("Server {server_id} does not exist.")))?;
2079        let srv = self.require_server(data, &server_id)?;
2080        let idp_type = srv
2081            .get("IdentityProviderType")
2082            .and_then(Value::as_str)
2083            .unwrap_or("SERVICE_MANAGED");
2084        let url = srv
2085            .get("IdentityProviderDetails")
2086            .and_then(|d| d.get("Url"))
2087            .and_then(Value::as_str)
2088            .unwrap_or("")
2089            .to_string();
2090        ok(json!({
2091            "Response": format!("{{\"IdentityProviderType\":\"{idp_type}\"}}"),
2092            "StatusCode": 200,
2093            "Message": "",
2094            "Url": url
2095        }))
2096    }
2097}
2098
2099// ===================== tagging =====================
2100
2101impl TransferService {
2102    fn tag_resource(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
2103        let resource_arn = req_str(b, "Arn")?.to_string();
2104        if b.get("Tags").and_then(Value::as_array).is_none() {
2105            return Err(invalid_request("Tags is required."));
2106        }
2107        let mut guard = self.state.write();
2108        let data = guard.get_or_create(&ctx.account);
2109        store_tags(data, &resource_arn, b);
2110        ok(json!({}))
2111    }
2112
2113    fn untag_resource(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
2114        let resource_arn = req_str(b, "Arn")?.to_string();
2115        let keys: Vec<String> = b
2116            .get("TagKeys")
2117            .and_then(Value::as_array)
2118            .map(|a| {
2119                a.iter()
2120                    .filter_map(|v| v.as_str().map(String::from))
2121                    .collect()
2122            })
2123            .ok_or_else(|| invalid_request("TagKeys is required."))?;
2124        let mut guard = self.state.write();
2125        let data = guard.get_or_create(&ctx.account);
2126        if let Some(entry) = data.tags.get_mut(&resource_arn) {
2127            for k in &keys {
2128                entry.remove(k);
2129            }
2130        }
2131        ok(json!({}))
2132    }
2133
2134    fn list_tags_for_resource(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
2135        let resource_arn = req_str(b, "Arn")?.to_string();
2136        let guard = self.state.read();
2137        let tags: Vec<Value> = guard
2138            .get(&ctx.account)
2139            .and_then(|d| d.tags.get(&resource_arn))
2140            .map(|m| {
2141                m.iter()
2142                    .map(|(k, v)| json!({ "Key": k, "Value": v }))
2143                    .collect()
2144            })
2145            .unwrap_or_default();
2146        // `ListTagsForResourceResponse` carries `MaxResults`/`NextToken`, so
2147        // honour the pagination window the validator already accepts.
2148        ok(list_response(
2149            "Tags",
2150            tags,
2151            b,
2152            &[("Arn", json!(resource_arn))],
2153        ))
2154    }
2155}