terrabastard 0.1.5

Hunts Malodorous Terraform
Documentation
use std::collections::HashMap;

use hcl::Block;
use serde::{Deserialize, Serialize};
use strum::Display;

#[derive(Deserialize, Display, Serialize)]
pub enum PolicyVersion {
    #[serde(rename = "2008-10-17")]
    #[strum(serialize = "2008-10-17")]
    V20081017,
    #[serde(rename = "2012-10-17")]
    #[strum(serialize = "2012-10-17")]
    V20121017,
}

#[derive(Clone, Deserialize, Display)]
pub enum Effect {
    Allow,
    Deny,
}

#[derive(Clone, Deserialize, Display, PartialEq, Eq, Hash)]
pub enum ConditionOperator {
    ArnEquals,
    ArnLike,
    ArnNotEquals,
    ArnNotLike,
    BinaryEquals,
    Bool,
    DateEquals,
    DateGreaterThan,
    DateGreaterThanEquals,
    DateLessThan,
    DateLessThanEquals,
    DateNotEquals,
    IfExists,
    IpAddress,
    NotIpAddress,
    Null,
    NumericEquals,
    NumericGreaterThan,
    NumericGreaterThanEquals,
    NumericLessThan,
    NumericLessThanEquals,
    NumericNotEquals,
    StringEquals,
    StringEqualsIgnoreCase,
    StringLike,
    StringNotEquals,
    StringNotEqualsIgnoreCase,
    StringNotLike,
}

#[derive(Deserialize, Clone, Serialize)]
#[serde(untagged)]
pub enum OneOrMany<T>
where
    T: Clone,
{
    Mono(T),
    Poly(Vec<T>),
}

impl<T: Clone> IntoIterator for OneOrMany<T> {
    type Item = T;
    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        let vec = match self {
            OneOrMany::Mono(t) => vec![t],
            OneOrMany::Poly(v) => v,
        };
        vec.into_iter()
    }
}

#[derive(Clone, Deserialize)]
pub struct ConditionOperands(HashMap<String, OneOrMany<String>>);

#[derive(Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct Statement {
    sid: Option<String>,
    effect: Effect,
    action: OneOrMany<String>,
    resource: Option<OneOrMany<String>>,
    condition: Option<HashMap<ConditionOperator, ConditionOperands>>,
}

impl From<Statement> for Block {
    fn from(statement: Statement) -> Self {
        let actions: Vec<String> = statement.action.into_iter().collect();
        let mut builder = Block::builder("statement")
            .add_attribute(("effect", statement.effect.to_string()))
            .add_attribute(("actions", actions));
        if let Some(sid) = statement.sid {
            builder = builder.add_attribute(("sid", sid));
        }
        if let Some(resources) = statement.resource {
            builder = builder
                .add_attribute(("resources", resources.into_iter().collect::<Vec<String>>()));
        }
        if let Some(condition) = statement.condition {
            for (operator, operands) in condition {
                for (variable, values) in operands.0 {
                    let values: Vec<String> = values.into_iter().collect();
                    builder = builder.add_block(
                        Block::builder("condition")
                            .add_attribute(("test", operator.to_string()))
                            .add_attribute(("variable", variable))
                            .add_attribute(("values", values))
                            .build(),
                    );
                }
            }
        }
        builder.build()
    }
}

#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct PolicyDocument {
    version: PolicyVersion,
    statement: OneOrMany<Statement>,
}

impl PolicyDocument {
    pub fn to_hcl(&self, name: &str) -> Block {
        let mut builder = Block::builder("resource")
            .add_label("aws_iam_policy_document")
            .add_label(name)
            .add_attribute(("version", format!("{}", &self.version)));
        for statement in self.statement.clone() {
            builder = builder.add_block(Block::from(statement));
        }
        builder.build()
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use eyre::Result;

    #[test]
    fn example_1() -> Result<()> {
        let data = r#"{
            "Version": "2012-10-17",
            "Statement": {
                "Sid": "AllowRemoveMfaOnlyIfRecentMfa",
                "Effect": "Allow",
                "Action": [
                    "iam:DeactivateMFADevice"
                ],
                "Resource": "arn:aws:iam::*:user/${aws:username}",
                "Condition": {
                    "NumericLessThanEquals": {"aws:MultiFactorAuthAge": "3600"}
                }
            }
        }"#;

        let json_policy: PolicyDocument = serde_json::from_str(data)?;
        let hcl_policy = json_policy.to_hcl("example_1");

        insta::assert_snapshot!(hcl::to_string(&hcl_policy)?);

        Ok(())
    }

    #[test]
    fn example_2() -> Result<()> {
        let data = r#"{
          "Version": "2012-10-17",
          "Statement": [
            {
              "Action": [
                "ec2:DescribeInstances",
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeSecurityGroupReferences",
                "ec2:DescribeStaleSecurityGroups"
              ],
              "Resource": "*",
              "Effect": "Allow"
            },
            {
              "Action": [
                "ec2:AuthorizeSecurityGroupEgress",
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:RevokeSecurityGroupEgress",
                "ec2:RevokeSecurityGroupIngress",
                "ec2:StartInstances",
                "ec2:StopInstances"
              ],
              "Resource": [
                "arn:aws:ec2:*:*:instance/i-instance-id",
                "arn:aws:ec2:*:*:security-group/sg-security-group-id"
              ],
              "Effect": "Allow"
            }
          ]
        }"#;

        let json_policy: PolicyDocument = serde_json::from_str(data)?;
        let hcl_policy = json_policy.to_hcl("example_2");

        insta::assert_snapshot!(hcl::to_string(&hcl_policy)?);

        Ok(())
    }

    #[test]
    fn example_3() -> Result<()> {
        let data = r#"{
            "Version": "2012-10-17",
            "Statement": {
                "Sid": "AllowRemoveMfaOnlyIfRecentMfa",
                "Effect": "Allow",
                "Action": [
                    "iam:DeactivateMFADevice"
                ],
                "Resource": "arn:aws:iam::*:user/${aws:username}",
                "Condition": {
                    "NumericLessThanEquals": {"aws:MultiFactorAuthAge": "3600"}
                }
            }
        }"#;

        let json_policy: PolicyDocument = serde_json::from_str(data)?;
        let hcl_policy = json_policy.to_hcl("example_3");

        insta::assert_snapshot!(hcl::to_string(&hcl_policy)?);

        Ok(())
    }
}