scratchstack-aws-principal 0.4.0

Principal types for AWS/AWS-like services
Documentation
use {
    crate::{utils::validate_name, PrincipalError},
    scratchstack_arn::{
        utils::{validate_account_id, validate_partition},
        Arn,
    },
    std::fmt::{Display, Formatter, Result as FmtResult},
};

/// Details about an assumed role actor.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct AssumedRole {
    /// The partition this principal exists in.
    partition: String,

    /// The account id.
    account_id: String,

    /// Name of the role, case-insensitive.
    role_name: String,

    /// Session name for the assumed role.
    session_name: String,
}

impl AssumedRole {
    /// Create an [AssumedRole] object.
    ///
    /// # Arguments:
    ///
    /// * `partition`: The partition this principal exists in.
    /// * `account_id`: The 12 digit account id. This must be composed of 12 ASCII digits or a
    ///     [PrincipalError::InvalidAccountId] error will be returned.
    /// * `role_name`: The name of the role being assumed. This must meet the following requirements or a
    ///     [PrincipalError::InvalidRoleName] error will be returned:
    ///     *   The name must contain between 1 and 64 characters.
    ///     *   The name must be composed to ASCII alphanumeric characters or one of `, - . = @ _`.
    /// * `session_name`: A name to assign to the session. This must meet the following requirements or a
    ///     [PrincipalError::InvalidSessionName] error will be returned:
    ///     *   The session name must contain between 2 and 64 characters.
    ///     *   The session name must be composed to ASCII alphanumeric characters or one of `, - . = @ _`.
    ///
    /// # Return value
    ///
    /// If all of the requirements are met, an [AssumedRole] object is returned. Otherwise,
    /// a [PrincipalError] error is returned.
    pub fn new(partition: &str, account_id: &str, role_name: &str, session_name: &str) -> Result<Self, PrincipalError> {
        validate_partition(partition)?;
        validate_account_id(account_id)?;
        validate_name(role_name, 64, PrincipalError::InvalidRoleName)?;
        validate_name(session_name, 64, PrincipalError::InvalidSessionName)?;

        if session_name.len() < 2 {
            Err(PrincipalError::InvalidSessionName(session_name.into()))
        } else {
            Ok(Self {
                partition: partition.into(),
                account_id: account_id.into(),
                role_name: role_name.into(),
                session_name: session_name.into(),
            })
        }
    }

    #[inline]
    pub fn partition(&self) -> &str {
        &self.partition
    }

    #[inline]
    pub fn account_id(&self) -> &str {
        &self.account_id
    }

    #[inline]
    pub fn role_name(&self) -> &str {
        &self.role_name
    }

    #[inline]
    pub fn session_name(&self) -> &str {
        &self.session_name
    }
}

impl From<&AssumedRole> for Arn {
    fn from(role: &AssumedRole) -> Arn {
        Arn::new(
            &role.partition,
            "sts",
            "",
            &role.account_id,
            &format!("assumed-role/{}/{}", role.role_name, role.session_name),
        )
        .unwrap()
    }
}

impl Display for AssumedRole {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        write!(
            f,
            "arn:{}:sts::{}:assumed-role/{}/{}",
            self.partition, self.account_id, self.role_name, self.session_name
        )
    }
}

#[cfg(test)]
mod tests {
    use {
        super::AssumedRole,
        crate::{PrincipalIdentity, PrincipalSource},
        scratchstack_arn::Arn,
        std::{
            collections::hash_map::DefaultHasher,
            hash::{Hash, Hasher},
        },
    };

    #[test]
    fn check_components() {
        let role = AssumedRole::new("aws", "123456789012", "role", "session").unwrap();
        assert_eq!(role.partition(), "aws");
        assert_eq!(role.account_id(), "123456789012");
        assert_eq!(role.role_name(), "role");
        assert_eq!(role.session_name(), "session");

        let arn: Arn = (&role).into();
        assert_eq!(arn.partition(), "aws");
        assert_eq!(arn.service(), "sts");
        assert_eq!(arn.region(), "");
        assert_eq!(arn.account_id(), "123456789012");
        assert_eq!(arn.resource(), "assumed-role/role/session");

        let p = PrincipalIdentity::from(role);
        let source = p.source();
        assert_eq!(source, PrincipalSource::Aws);
        assert_eq!(source.to_string(), "AWS".to_string());
    }

    #[test]
    fn check_derived() {
        let r1a = AssumedRole::new("aws", "123456789012", "role1", "session1").unwrap();
        let r1b = AssumedRole::new("aws", "123456789012", "role1", "session1").unwrap();
        let r2 = AssumedRole::new("aws", "123456789012", "role1", "session2").unwrap();
        let r3 = AssumedRole::new("aws", "123456789012", "role2", "session2").unwrap();
        let r4 = AssumedRole::new("aws", "123456789013", "role2", "session2").unwrap();
        let r5 = AssumedRole::new("awt", "123456789013", "role2", "session2").unwrap();

        assert_eq!(r1a, r1b);
        assert_ne!(r1a, r2);
        assert_eq!(r1a.clone(), r1a);

        // Ensure we can hash an assumed role.
        let mut h1a = DefaultHasher::new();
        let mut h1b = DefaultHasher::new();
        let mut h2 = DefaultHasher::new();
        r1a.hash(&mut h1a);
        r1b.hash(&mut h1b);
        r2.hash(&mut h2);
        let hash1a = h1a.finish();
        let hash1b = h1b.finish();
        let hash2 = h2.finish();
        assert_eq!(hash1a, hash1b);
        assert_ne!(hash1a, hash2);

        // Ensure ordering is logical.
        assert!(r1a <= r1b);
        assert!(r1a < r2);
        assert!(r2 < r3);
        assert!(r3 > r2);
        assert!(r3 > r1a);
        assert!(r3 < r4);
        assert!(r4 > r3);
        assert!(r4 < r5);
        assert!(r5 > r4);

        assert_eq!(r1a.clone().max(r2.clone()), r2);
        assert_eq!(r1a.clone().min(r2), r1a);

        // Ensure formatting is correct to an ARN.
        assert_eq!(r1a.to_string(), "arn:aws:sts::123456789012:assumed-role/role1/session1");

        // Ensure we can debug print an assumed role.
        let _ = format!("{:?}", r1a);
    }

    #[test]
    fn check_valid_assumed_roles() {
        let r1a = AssumedRole::new("aws", "123456789012", "Role_name", "session_name").unwrap();
        let r1b = AssumedRole::new("aws", "123456789012", "Role_name", "session_name").unwrap();
        let r2 =
            AssumedRole::new("aws2", "123456789012", "Role@Foo=bar,baz_=world-1234", "Session@1234,_=-,.OK").unwrap();

        assert_eq!(r1a, r1b);
        assert_ne!(r1a, r2);
        assert!(r1a <= r1b);
        assert!(r1a >= r1b);
        assert_eq!(r1a.partition(), "aws");
        assert_eq!(r1a.account_id(), "123456789012");
        assert_eq!(r1a.role_name(), "Role_name");
        assert_eq!(r1a.session_name(), "session_name");

        assert!(r1a < r2);
        assert!(r1a <= r2);
        assert!(r2 > r1a);
        assert!(r2 >= r1a);
        assert!(r2 != r1a);

        assert_eq!(r1a.to_string(), "arn:aws:sts::123456789012:assumed-role/Role_name/session_name");
        assert_eq!(r1b.to_string(), "arn:aws:sts::123456789012:assumed-role/Role_name/session_name");
        assert_eq!(
            r2.to_string(),
            "arn:aws2:sts::123456789012:assumed-role/Role@Foo=bar,baz_=world-1234/Session@1234,_=-,.OK"
        );

        let r1c = r1a.clone();
        assert!(r1a == r1c);

        AssumedRole::new("partition-with-32-characters1234", "123456789012", "role-name", "session_name").unwrap();

        AssumedRole::new(
            "aws",
            "123456789012",
            "role-name-with_64-characters====================================",
            "session@1234",
        )
        .unwrap();

        AssumedRole::new(
            "aws",
            "123456789012",
            "role-name",
            "session-name-with-64-characters=================================",
        )
        .unwrap();

        // Make sure we can debug the assumed role.
        let _ = format!("{:?}", r1a);
    }

    #[test]
    fn check_invalid_assumed_roles() {
        let err = AssumedRole::new("", "123456789012", "role-name", "session-name").unwrap_err();
        assert_eq!(err.to_string(), r#"Invalid partition: """#);

        let err = AssumedRole::new("partition-with-33-characters12345", "123456789012", "role-name", "session_name")
            .unwrap_err();
        assert_eq!(err.to_string(), r#"Invalid partition: "partition-with-33-characters12345""#);

        let err = AssumedRole::new("-aws", "123456789012", "role-name", "session-name").unwrap_err();
        assert_eq!(err.to_string(), r#"Invalid partition: "-aws""#);

        let err = AssumedRole::new("aws-", "123456789012", "role-name", "session-name").unwrap_err();
        assert_eq!(err.to_string(), r#"Invalid partition: "aws-""#);

        let err = AssumedRole::new("aws--us", "123456789012", "role-name", "session-name").unwrap_err();
        assert_eq!(err.to_string(), r#"Invalid partition: "aws--us""#);

        let err = AssumedRole::new("aw!", "123456789012", "role-name", "session-name").unwrap_err();
        assert_eq!(err.to_string(), r#"Invalid partition: "aw!""#);

        let err = AssumedRole::new("aws", "", "role-name", "session-name").unwrap_err();
        assert_eq!(err.to_string(), r#"Invalid account id: """#);

        let err = AssumedRole::new("aws", "a23456789012", "role-name", "session-name").unwrap_err();
        assert_eq!(err.to_string(), r#"Invalid account id: "a23456789012""#);

        let err = AssumedRole::new("aws", "1234567890123", "role-name", "session-name").unwrap_err();
        assert_eq!(err.to_string(), r#"Invalid account id: "1234567890123""#);

        let err = AssumedRole::new("aws", "123456789012", "", "session-name").unwrap_err();
        assert_eq!(err.to_string(), r#"Invalid role name: """#);

        let err = AssumedRole::new(
            "aws",
            "123456789012",
            "role-name-with-65-characters=====================================",
            "session-name",
        )
        .unwrap_err();
        assert_eq!(
            err.to_string(),
            r#"Invalid role name: "role-name-with-65-characters=====================================""#
        );

        let err = AssumedRole::new("aws", "123456789012", "role+name", "session-name").unwrap_err();
        assert_eq!(err.to_string(), r#"Invalid role name: "role+name""#);

        let err = AssumedRole::new("aws", "123456789012", "role-name", "").unwrap_err();
        assert_eq!(err.to_string(), r#"Invalid session name: """#);

        let err = AssumedRole::new("aws", "123456789012", "role-name", "s").unwrap_err();
        assert_eq!(err.to_string(), r#"Invalid session name: "s""#);

        let err = AssumedRole::new(
            "aws",
            "123456789012",
            "role-name",
            "session-name-with-65-characters==================================",
        )
        .unwrap_err();

        assert_eq!(
            err.to_string(),
            r#"Invalid session name: "session-name-with-65-characters==================================""#
        );

        let err = AssumedRole::new("aws", "123456789012", "role-name", "session+name").unwrap_err();
        assert_eq!(err.to_string(), r#"Invalid session name: "session+name""#);
    }
}
// end tests -- do not delete; needed for coverage.