wami-core 0.18.0

Core primitives for wami: ARNs, contexts, errors and shared types
Documentation
use super::{ArnTransformer, ProviderArnInfo};
use crate::arn::types::{Service, WamiArn};
use crate::error::{AmiError, Result};

/// AWS ARN transformer.
///
/// Converts between WAMI ARNs and AWS ARN format:
/// `arn:aws:{service}::{account_id}:{resource_type}/{resource_id}`
///
/// # Examples
///
/// ```
/// use wami_core::arn::{WamiArn, Service, AwsArnTransformer, ArnTransformer};
///
/// let arn = WamiArn::builder()
///     .service(Service::Iam)
///     .tenant(12345678)
///     .wami_instance("999888777")
///     .cloud_provider("aws", "223344556677")
///     .resource("user", "77557755")
///     .build()
///     .unwrap();
///
/// let transformer = AwsArnTransformer;
/// let aws_arn = transformer.to_provider_arn(&arn).unwrap();
/// assert_eq!(aws_arn, "arn:aws:iam::223344556677:user/77557755");
/// ```
pub struct AwsArnTransformer;

impl ArnTransformer for AwsArnTransformer {
    fn to_provider_arn(&self, arn: &WamiArn) -> Result<String> {
        let cloud_mapping =
            arn.cloud_mapping
                .as_ref()
                .ok_or_else(|| AmiError::InvalidParameter {
                    message: "ARN is not cloud-synced".to_string(),
                })?;

        if cloud_mapping.provider != "aws" {
            return Err(AmiError::InvalidParameter {
                message: format!(
                    "ARN provider is '{}', expected 'aws'",
                    cloud_mapping.provider
                ),
            });
        }

        // Map WAMI service to AWS service
        let aws_service = match &arn.service {
            Service::Iam => "iam",
            Service::Sts => "sts",
            Service::SsoAdmin => "sso",
            Service::Custom(s) => s.as_str(),
        };

        // AWS ARN format: arn:aws:service:region:account-id:resource
        // For global services like IAM, region is empty
        let region = cloud_mapping.region.as_deref().unwrap_or("");

        Ok(format!(
            "arn:aws:{}:{}:{}:{}/{}",
            aws_service,
            region,
            cloud_mapping.account_id,
            arn.resource.resource_type,
            arn.resource.resource_id
        ))
    }

    fn from_provider_arn(&self, provider_arn: &str) -> Result<ProviderArnInfo> {
        let parts: Vec<&str> = provider_arn.split(':').collect();

        if parts.len() < 6 {
            return Err(AmiError::InvalidParameter {
                message: format!(
                    "Invalid AWS ARN format: expected at least 6 parts, got {}",
                    parts.len()
                ),
            });
        }

        if parts[0] != "arn" || parts[1] != "aws" {
            return Err(AmiError::InvalidParameter {
                message: format!(
                    "Invalid AWS ARN prefix: expected 'arn:aws', got '{}:{}'",
                    parts[0], parts[1]
                ),
            });
        }

        let service = parts[2].to_string();
        let region = parts[3].to_string(); // Region (may be empty for global services)
        let account_id = parts[4].to_string();

        // Resource is everything after account_id
        let resource_part = parts[5..].join(":");
        let resource_parts: Vec<&str> = resource_part.split('/').collect();

        if resource_parts.len() < 2 {
            return Err(AmiError::InvalidParameter {
                message: format!(
                    "Invalid AWS ARN resource format: expected 'type/id', got '{}'",
                    resource_part
                ),
            });
        }

        let resource_type = resource_parts[0].to_string();
        let resource_id = resource_parts[1..].join("/");

        Ok(ProviderArnInfo {
            provider: "aws".to_string(),
            account_id,
            service,
            resource_type,
            resource_id,
            region: if region.is_empty() {
                None
            } else {
                Some(region)
            },
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::arn::{Service, WamiArn};
    #[test]
    fn test_aws_transformer() {
        let arn = WamiArn::builder()
            .service(Service::Iam)
            .tenant(12345678)
            .wami_instance("999888777")
            .cloud_provider("aws", "223344556677")
            .resource("user", "77557755")
            .build()
            .unwrap();

        let transformer = AwsArnTransformer;
        let aws_arn = transformer.to_provider_arn(&arn).unwrap();
        assert_eq!(aws_arn, "arn:aws:iam::223344556677:user/77557755");

        let info = transformer.from_provider_arn(&aws_arn).unwrap();
        assert_eq!(info.provider, "aws");
        assert_eq!(info.account_id, "223344556677");
        assert_eq!(info.service, "iam");
        assert_eq!(info.resource_type, "user");
        assert_eq!(info.resource_id, "77557755");
    }

    #[test]
    fn test_aws_transformer_wrong_provider() {
        let arn = WamiArn::builder()
            .service(Service::Iam)
            .tenant(12345678)
            .wami_instance("999888777")
            .cloud_provider("gcp", "123456")
            .resource("user", "77557755")
            .build()
            .unwrap();

        let transformer = AwsArnTransformer;
        let result = transformer.to_provider_arn(&arn);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("expected 'aws'"));
    }

    #[test]
    fn test_aws_transformer_not_cloud_synced() {
        let arn = WamiArn::builder()
            .service(Service::Iam)
            .tenant(12345678)
            .wami_instance("999888777")
            .resource("user", "77557755")
            .build()
            .unwrap();

        let transformer = AwsArnTransformer;
        let result = transformer.to_provider_arn(&arn);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("not cloud-synced"));
    }

    #[test]
    fn test_aws_sts_service() {
        let arn = WamiArn::builder()
            .service(Service::Sts)
            .tenant(12345678)
            .wami_instance("999888777")
            .cloud_provider("aws", "223344556677")
            .resource("assumed-role", "role123")
            .build()
            .unwrap();

        let transformer = AwsArnTransformer;
        let aws_arn = transformer.to_provider_arn(&arn).unwrap();
        assert_eq!(aws_arn, "arn:aws:sts::223344556677:assumed-role/role123");
    }

    #[test]
    fn test_aws_resource_with_slash() {
        let arn = WamiArn::builder()
            .service(Service::Iam)
            .tenant(12345678)
            .wami_instance("999888777")
            .cloud_provider("aws", "223344556677")
            .resource("policy", "path/to/policy")
            .build()
            .unwrap();

        let transformer = AwsArnTransformer;
        let aws_arn = transformer.to_provider_arn(&arn).unwrap();
        assert_eq!(aws_arn, "arn:aws:iam::223344556677:policy/path/to/policy");

        let info = transformer.from_provider_arn(&aws_arn).unwrap();
        assert_eq!(info.resource_id, "path/to/policy");
    }

    #[test]
    fn test_aws_transformer_with_region() {
        let arn = WamiArn::builder()
            .service(Service::Iam)
            .tenant(12345678)
            .wami_instance("999888777")
            .cloud_provider_with_region("aws", "223344556677", "us-east-1")
            .resource("user", "alice")
            .build()
            .unwrap();

        let transformer = AwsArnTransformer;
        let aws_arn = transformer.to_provider_arn(&arn).unwrap();
        // The implementation includes region in the ARN format
        assert_eq!(aws_arn, "arn:aws:iam:us-east-1:223344556677:user/alice");
    }

    #[test]
    fn test_aws_transformer_invalid_arn_format() {
        let transformer = AwsArnTransformer;
        let result = transformer.from_provider_arn("not-an-arn");
        assert!(result.is_err());
    }

    #[test]
    fn test_aws_transformer_from_arn_edge_cases() {
        let transformer = AwsArnTransformer;

        // ARN with assumed role
        let aws_arn = "arn:aws:sts::123456789012:assumed-role/MyRole/session";
        let info = transformer.from_provider_arn(aws_arn).unwrap();
        assert_eq!(info.provider, "aws");
        assert_eq!(info.account_id, "123456789012");
        assert_eq!(info.service, "sts");
        assert_eq!(info.resource_type, "assumed-role");
        assert_eq!(info.resource_id, "MyRole/session");
        assert_eq!(info.region, None);

        // ARN with region (for services that support it)
        let aws_arn = "arn:aws:s3:us-east-1::bucket/my-bucket";
        let info = transformer.from_provider_arn(aws_arn).unwrap();
        assert_eq!(info.service, "s3");
        assert_eq!(info.region, Some("us-east-1".to_string()));
    }
}