scratchstack-aspen 0.3.1

AWS IAM policy language (Aspen) parser and internal representation
Documentation
use {
    super::AwsPrincipal,
    crate::{display_json, from_str_json, serutil::StringLikeList},
    derive_builder::Builder,
    scratchstack_aws_principal::{Principal as PrincipalActor, PrincipalIdentity, PrincipalSource},
    serde::{Deserialize, Serialize},
};

/// A non-wildcard principal statement in an Aspen policy.
///
/// SpecifiedPrincipal structs are immutable. To construct this programmatically, use [SpecifiedPrincipalBuilder].
#[derive(Builder, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct SpecifiedPrincipal {
    /// AWS principals specified in the statement.
    #[builder(setter(into, strip_option), default)]
    #[serde(rename = "AWS", skip_serializing_if = "Option::is_none")]
    aws: Option<StringLikeList<AwsPrincipal>>,

    /// Canonical users specified in the statement.
    #[builder(setter(into, strip_option), default)]
    #[serde(rename = "CanonicalUser", skip_serializing_if = "Option::is_none")]
    canonical_user: Option<StringLikeList<String>>,

    /// Federated users specified in the statement.
    #[builder(setter(into, strip_option), default)]
    #[serde(rename = "Federated", skip_serializing_if = "Option::is_none")]
    federated: Option<StringLikeList<String>>,

    /// Services specified in the statement.
    #[builder(setter(into, strip_option), default)]
    #[serde(rename = "Service", skip_serializing_if = "Option::is_none")]
    service: Option<StringLikeList<String>>,
}

display_json!(SpecifiedPrincipal);
from_str_json!(SpecifiedPrincipal);

impl SpecifiedPrincipal {
    /// Create a [SpecifiedPrincipalBuilder] to programmatically construct a [SpecifiedPrincipal].
    #[inline]
    pub fn builder() -> SpecifiedPrincipalBuilder {
        SpecifiedPrincipalBuilder::default()
    }

    /// Returns the AWS principals specified in the statement.
    #[inline]
    pub fn aws(&self) -> Option<&StringLikeList<AwsPrincipal>> {
        self.aws.as_ref()
    }

    /// Returns the canonical users specified in the statement.
    #[inline]
    pub fn canonical_user(&self) -> Option<&StringLikeList<String>> {
        self.canonical_user.as_ref()
    }

    /// Returns the federated users specified in the statement.
    #[inline]
    pub fn federated(&self) -> Option<&StringLikeList<String>> {
        self.federated.as_ref()
    }

    /// Returns the services specified in the statement.
    #[inline]
    pub fn service(&self) -> Option<&StringLikeList<String>> {
        self.service.as_ref()
    }

    /// Indicates whether this specified principal matches an identity from the [PrincipalActor].
    pub fn matches(&self, actor: &PrincipalActor) -> bool {
        for identity in actor.iter() {
            let source = identity.source();
            match source {
                PrincipalSource::Aws => {
                    if let Some(aws_ids) = self.aws() {
                        for aws_id in aws_ids.iter() {
                            if aws_id.matches(identity) {
                                return true;
                            }
                        }
                    }
                }
                PrincipalSource::CanonicalUser => {
                    if let PrincipalIdentity::CanonicalUser(identity) = identity {
                        if let Some(canonical_users) = self.canonical_user() {
                            for canonical_user in canonical_users.iter() {
                                if canonical_user == identity.canonical_user_id() {
                                    return true;
                                }
                            }
                        }
                    }
                }
                PrincipalSource::Federated => {
                    if let PrincipalIdentity::FederatedUser(identity) = identity {
                        if let Some(federated) = self.federated() {
                            for federated in federated.iter() {
                                if federated == identity.user_name() {
                                    return true;
                                }
                            }
                        }
                    }
                }
                PrincipalSource::Service => {
                    if let PrincipalIdentity::Service(identity) = identity {
                        if let Some(services) = self.service() {
                            for service in services.iter() {
                                if service == identity.global_dns_name().as_str()
                                    || service == identity.regional_dns_name().as_str()
                                {
                                    return true;
                                }
                            }
                        }
                    }
                }
            }
        }

        false
    }
}

#[cfg(test)]
mod tests {
    use {
        super::SpecifiedPrincipal,
        scratchstack_aws_principal::{
            CanonicalUser, FederatedUser, Principal as PrincipalActor, PrincipalIdentity, Service, User,
        },
        std::str::FromStr,
    };

    #[test_log::test]
    fn test_deserialize_basic1() {
        let sp = SpecifiedPrincipal::from_str(
            r#"
            {
                "AWS": ["123456789012", "arn:aws:iam::123456789012:user/dacut"],
                "CanonicalUser": ["df22d4799ef444d6434c676951d8b390145f2fc5f9107140d0e4b733ad40516d"],
                "Federated": ["dacut@kanga.org"],
                "Service": ["ec2.amazonaws.com", "lambda.amazonaws.com"]
            }
        "#,
        )
        .unwrap();

        assert!(sp.aws.is_some());
        assert!(sp.canonical_user.is_some());
        assert!(sp.federated.is_some());
        assert!(sp.service.is_some());
    }

    #[test_log::test]
    fn test_matches() {
        let sp = SpecifiedPrincipal::from_str(
            r#"
            {
                "AWS": ["arn:aws:iam::123456789012:user/dacut"],
                "CanonicalUser": ["df22d4799ef444d6434c676951d8b390145f2fc5f9107140d0e4b733ad40516d"],
                "Federated": ["dacut@kanga.org"],
                "Service": ["ec2.amazonaws.com", "lambda.amazonaws.com"]
            }
        "#,
        )
        .unwrap();

        let user = User::new("aws", "123456789012", "/", "dacut").unwrap();
        let canonical_user =
            CanonicalUser::new("df22d4799ef444d6434c676951d8b390145f2fc5f9107140d0e4b733ad40516d").unwrap();
        let federated_user = FederatedUser::new("aws", "123456789012", "dacut@kanga.org").unwrap();
        let lambda_regional = Service::new("lambda", Some("us-east-1".to_string()), "amazonaws.com").unwrap();

        assert!(sp.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(user.clone())])));
        assert!(sp.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(canonical_user.clone())])));
        assert!(sp.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(federated_user.clone())])));
        assert!(sp.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(lambda_regional.clone())])));

        let user_wrong_partition = User::new("aws-us-gov", "123456789012", "/", "dacut").unwrap();
        assert!(!sp.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(user_wrong_partition)])));

        let wrong_canonical_user =
            CanonicalUser::new("0000000000000000000000000000000000000000000000000000000000000000").unwrap();
        assert!(!sp.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(wrong_canonical_user)])));

        let wrong_federated_user = FederatedUser::new("aws", "123456789012", "evildoer@kanga.org").unwrap();
        assert!(!sp.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(wrong_federated_user)])));

        let wrong_service = Service::new("s3", None, "amazonaws.com").unwrap();
        assert!(!sp.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(wrong_service)])));

        let empty = SpecifiedPrincipal::builder().build().unwrap();
        assert!(!empty.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(user)])));
        assert!(!empty.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(canonical_user)])));
        assert!(!empty.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(federated_user)])));
        assert!(!empty.matches(&PrincipalActor::from(vec![PrincipalIdentity::from(lambda_regional)])));
    }
}