diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl new file mode 100644 index 0000000..6e7e533 --- /dev/null +++ b/.terraform.lock.hcl @@ -0,0 +1,2 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. diff --git a/README.md b/README.md index c08297b..3bd870a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,90 @@ -# terraform-aws-client-vpn-authorization-rules -Calculates and creates a minimal set of AWS Client VPN authorization rules that meet given requirements. +# Terraform AWS Client VPN Authorization Rule Calculator + +This module takes a list of Client VPN authorization rules for zero or more groups and returns a minimal set of authorization rules that is functionally equivalent. This is useful for handling the Client VPN [longest prefix path](https://docs.aws.amazon.com/vpn/latest/clientvpn-admin/cvpn-working-rules.html) networking, which can lead to unexpected behaviour as documented [here](https://docs.aws.amazon.com/vpn/latest/clientvpn-admin/troubleshooting.html#ad-group-auth-rules). It also minimizes the number of authorization rules that need to be created by merging rules where possible and eliminating redundant rules. + +Note that this module **does not** actually create the rules. The set of inputs for rules is returned, and the user can then create the rules or do something else with the result. + +The module works by: +1. For each group (considering "all users" rules as a separate group), reduce the rules to the minimum equivalent set by merging CIDR blocks where possible (using the [Invicton-Labs/merge-cidrs/null](https://registry.terraform.io/modules/Invicton-Labs/merge-cidrs/null/latest) module). +2. Remove any rules where the same effect is achieved by an "all-users" rule, as they are redundant. +3. For each remaining rule, if a different group has a rule within the first rule's prefix (but with a longer prefix length), add a rule with the same prefix to the first group. This handles funny behaviour with longest-prefix evaluation. +4. Eliminate any duplicate rules within a single group that may have occurred if the same longer prefix rule was present in multiple other groups (a corresponding one would have been added for each). +5. Eliminate any rules within a group where other, longer-prefix rules within the same group combine to be functionally equivalent. + + +Example: +```terraform +module "vpn_authorization_rules" { + source = "Invicton-Labs/client-vpn-authorization-rules/aws" + authorization_rules = [ + { + description = "Admin 1" + target_network_cidr = "1.0.0.0/16" + access_group_id = "admin" + }, + { + description = "Everyone 1" + authorize_all_groups = true + target_network_cidr = "1.0.0.0/17" + }, + { + description = "Dev 1" + target_network_cidr = "1.0.0.0/18" + access_group_id = "dev" + }, + { + description = "Dev 2" + target_network_cidr = "1.0.128.0/19" + access_group_id = "dev" + }, + { + description = "Dev 3" + target_network_cidr = "1.0.160.0/19" + access_group_id = "dev" + }, + ] +} + +output "merged_authorization_rules" { + value = module.vpn_authorization_rules.merged_authorization_rules +} +``` + +``` +$ terraform apply -auto-approve + +Apply complete! Resources: 0 added, 0 changed, 0 destroyed. + +Outputs: + +Apply complete! Resources: 0 added, 0 changed, 0 destroyed. + +Outputs: + +merged_authorization_rules = { + "admin|1.0.0.0/16" = { + "access_group_id" = "admin" + "authorize_all_groups" = tobool(null) + "description" = "Admin 1 [1.0.0.0/16]" + "target_network_cidr" = "1.0.0.0/16" + } + "admin|1.0.128.0/18" = { + "access_group_id" = "admin" + "authorize_all_groups" = tobool(null) + "description" = "Admin 1 [1.0.0.0/16] (covering longer prefix path from \"Dev 2 [1.0.128.0/19]; Dev 3 [1.0.160.0/19]\")" + "target_network_cidr" = "1.0.128.0/18" + } + "dev|1.0.128.0/18" = { + "access_group_id" = "dev" + "authorize_all_groups" = tobool(null) + "description" = "Dev 2 [1.0.128.0/19]; Dev 3 [1.0.160.0/19]" + "target_network_cidr" = "1.0.128.0/18" + } + "eb8325b4-c0c4-4b49-962f-71a5949efb24|1.0.0.0/17" = { + "access_group_id" = tostring(null) + "authorize_all_groups" = true + "description" = "Everyone 1 [1.0.0.0/17]" + "target_network_cidr" = "1.0.0.0/17" + } +} +``` diff --git a/main.tf b/main.tf index d2e915c..35dda8e 100644 --- a/main.tf +++ b/main.tf @@ -38,6 +38,8 @@ For each rule: 2. If there's nothing smaller within the CIDR or larger that covers the CIDR, AND there's an everyone rule that covers it, delete it (done in rules_with_everyone_duplicates_removed) 3. If there's a smaller one within the CIDR for another group, add that smaller one specifically (done in rules_with_additional_cidrs) 4. If a larger CIDR is entirely completed by smaller CIDRs that are required, delete the larger one (done in rules_with_unnecessary_larger_removed) + +NOTE: we use ternary operators instead of &&/|| throughout because it enables lazy evaluation, thereby speeding up the process by not running unnecessary checks. */ // Start by reducing all rules within each group to the minimum set. @@ -67,7 +69,7 @@ locals { { group_id = group_id cidr = cidr_data.cidr - description = join(var.merged_rule_description_joiner, [for contained in cidr_data.contains : contained.metadata.description]) + description = join(var.merged_rule_description_joiner, [for contained in cidr_data.contains : "${contained.metadata.description} [${contained.cidr}]"]) first_ip = cidrhost(cidr_data.cidr, 0) last_ip = cidrhost(cidr_data.cidr, pow(2, 32 - tonumber(split("/", cidr_data.cidr)[1])) - 1) } @@ -86,76 +88,42 @@ locals { ] } - // Add a field that indicates if this rule is covered by an everyone group rule, - // i.e. if an everyone rule would provide the same access if this rule didn't exist. - rules_with_everyone_meta = { + // Remove any rules that are covered by "everyone" rules. This is possible because: + // - any smaller rules that are subsets of a given rule will also be removed if the given rule + // is removed, as it will also be covered by the same everyone rule. Therefore, there is no + // need to duplicate the longer prefix path of that smaller rule. + // - any larger rule that subsumes a given rule, which is also covered by an everyone rule, + // will also be removed. Therefore, there is no reason to keep this smaller prefix path rule. + // - any larger rule that is larger than the everyone rule will not be considered in AWS routing + // evaluation, as the everyone rule will have a longer prefix length and will be considered first. + rules_with_everyone_duplicates_removed = contains(keys(local.rules_with_first_last_decimal), local.everyone_group_uuid) ? { for group_id, rules in local.rules_with_first_last_decimal : - group_id => [ - for rule in rules : - merge(rule, { - covered_by_everyone_rule = length([ - for everyone_rule in local.rules_with_first_last_decimal[local.everyone_group_uuid] : - true - if( - rule.group_id != local.everyone_group_uuid ? ( - everyone_rule.first_ip_decimal <= rule.first_ip_decimal ? ( - everyone_rule.last_ip_decimal >= rule.last_ip_decimal - ) : false - ) : false - ) - ]) > 0 - }) - ] - } - - // Create a flat list of all rules across all groups. - all_rules_with_everyone_meta = concat(values(local.rules_with_everyone_meta)...) - - // Remove any rules where the same access is granted by an everyone rule. - // There are many conditions to doing this, check the comments within. - rules_with_everyone_duplicates_removed = { - for group_id, rules in local.rules_with_everyone_meta : group_id => [ for rule in rules : rule if( // If it's an everyone group rule, it has to remain - group_id == local.everyone_group_uuid || - // Always include it if it's not subsumed by an everyone rule - !rule.covered_by_everyone_rule || - // OR, if there's any other CIDR from a different group that includes this one, or is a part of this one. - length([ - for compare_rule in local.all_rules_with_everyone_meta : - true - if( - // The rule has to be for a different group to qualify - compare_rule.group_id != rule.group_id && - // That other group can't be the everyone group, since we're considering deleting in favour of the everyone group rule - compare_rule.group_id != local.everyone_group_uuid && - // The rule can't be one that is covered by an everyone rule, since we'd like to disappear that rule too. - !compare_rule.covered_by_everyone_rule && - ( - // The other rule subsumes this rule, OR - (compare_rule.first_ip_decimal <= rule.first_ip_decimal && compare_rule.last_ip_decimal >= rule.last_ip_decimal) || - // The other rule is subsumed by this one - (rule.first_ip_decimal <= compare_rule.first_ip_decimal && rule.last_ip_decimal >= compare_rule.last_ip_decimal) - ) && - // There can't be an everyone rule that is the same size or smaller than the compare rule - length([ - for everyone_rule in local.rules_with_first_last_decimal[local.everyone_group_uuid] : - true - if( - // The everyone rule has to subsume this rule - (everyone_rule.first_ip_decimal <= rule.first_ip_decimal && everyone_rule.last_ip_decimal >= rule.last_ip_decimal) && - // The everyone rule has to be subsumed by the compare rule - (compare_rule.first_ip_decimal <= everyone_rule.first_ip_decimal && compare_rule.last_ip_decimal >= everyone_rule.last_ip_decimal) - ) - ]) == 0 - ) - ]) > 0 + group_id == local.everyone_group_uuid ? true : ( + // Always include it if it's not subsumed by an everyone rule + length([ + for everyone_rule in local.rules_with_first_last_decimal[local.everyone_group_uuid] : + true + if( + rule.group_id != local.everyone_group_uuid ? ( + everyone_rule.first_ip_decimal <= rule.first_ip_decimal ? ( + everyone_rule.last_ip_decimal >= rule.last_ip_decimal + ) : false + ) : false + ) + ]) == 0 + ) ) ] - } + // If there are no everyone rules, skip and move on + } : local.rules_with_first_last_decimal + + // Create a flat list of all rules across all groups. + all_rules_with_everyone_meta = concat(values(local.rules_with_everyone_duplicates_removed)...) // For each rule, add additional rules to match any longer-prefix rules for other groups. rules_with_additional_cidrs = { @@ -169,20 +137,23 @@ locals { ], [ for compare_rule in local.all_rules_with_everyone_meta : merge(compare_rule, { - description = "${rule.description} (covering longest prefix path from \"${compare_rule.description}\")" + description = "${rule.description} (covering longer prefix path from \"${compare_rule.description}\")" group_id = rule.group_id extra_due_to_other_group = true }) if( // Only consider rules from other groups - compare_rule.group_id != rule.group_id && - // That other group can't be the everyone group, since the everyone group would provide access - // to this group as well anyways. - compare_rule.group_id != local.everyone_group_uuid && - // We don't need to add a duplicate of the compare rule for this rule if it's identical, since that wouldn't accomplish anything. - !(compare_rule.first_ip_decimal == rule.first_ip_decimal && compare_rule.last_ip_decimal == rule.last_ip_decimal) && - // The compare rule needs to be subsumed by this rule (longer prefix length). - (rule.first_ip_decimal <= compare_rule.first_ip_decimal && rule.last_ip_decimal >= compare_rule.last_ip_decimal) + compare_rule.group_id != rule.group_id ? ( + // That other group can't be the everyone group, since the everyone group would provide access + // to this group as well anyways. + compare_rule.group_id != local.everyone_group_uuid ? ( + // We don't need to add a duplicate of the compare rule for this rule if it's identical, since that wouldn't accomplish anything. + !(compare_rule.first_ip_decimal == rule.first_ip_decimal ? compare_rule.last_ip_decimal == rule.last_ip_decimal : false) ? ( + // The compare rule needs to be subsumed by this rule (longer prefix length). + (rule.first_ip_decimal <= compare_rule.first_ip_decimal ? rule.last_ip_decimal >= compare_rule.last_ip_decimal : false) + ) : false + ) : false + ) : false ) ]) ]) @@ -230,14 +201,15 @@ locals { rule if( // Keep it if it had to be added due to a smaller prefix in another group - rule.extra_due_to_other_group || - ( - // Keep it if none of the merged CIDRs of required added rules subsume this CIDR - length([ - for compare_rule in module.merge_cidr_for_redundancy_check.merged_cidr_sets_ipv4_with_meta[group_id] : - true - if(compare_rule.first_ip_decimal <= rule.first_ip_decimal && compare_rule.last_ip_decimal >= rule.last_ip_decimal) - ]) == 0 + rule.extra_due_to_other_group ? true : ( + ( + // Keep it if none of the merged CIDRs of required added rules subsume this CIDR + length([ + for compare_rule in module.merge_cidr_for_redundancy_check.merged_cidr_sets_ipv4_with_meta[group_id] : + true + if(compare_rule.first_ip_decimal <= rule.first_ip_decimal ? compare_rule.last_ip_decimal >= rule.last_ip_decimal : false) + ]) == 0 + ) ) ) ] @@ -253,16 +225,4 @@ locals { description = rule.description } } - - # TODO: replace and/or with ternary } - -// If desired, create the rules -# resource "aws_ec2_client_vpn_authorization_rule" "this" { -# for_each = var.create_rules ? local.all_rules : {} -# client_vpn_endpoint_id = var.client_vpn_endpoint_id -# target_network_cidr = each.value.target_network_cidr -# authorize_all_groups = each.value.authorize_all_groups -# access_group_id = each.value.access_group_id -# description = each.value.description -# } diff --git a/outputs.tf b/outputs.tf index a0b3c85..fbdd77a 100644 --- a/outputs.tf +++ b/outputs.tf @@ -5,10 +5,6 @@ output "authorization_rules" { description = "The value of the `authorization_rules` input variable." value = var.authorization_rules } -output "client_vpn_endpoint_id" { - description = "The value of the `client_vpn_endpoint_id` input variable." - value = var.client_vpn_endpoint_id -} output "merged_rule_description_joiner" { description = "The value of the `merged_rule_description_joiner` input variable, or the default value if the input was `null`." value = var.merged_rule_description_joiner @@ -21,10 +17,6 @@ output "merged_authorization_rules" { description = "The reduced/merged inputs that can/will be used to create the actual rules." value = local.all_rules } -# output "authorization_rule_resources" { -# description = "The aws_ec2_client_vpn_authorization_rule resources that were created, if the `create_rules` input variable was `true` (otherwise, `null`)." -# value = var.create_rules ? aws_ec2_client_vpn_authorization_rule.this : null -# } //================================================== // Debugging outputs @@ -44,21 +36,18 @@ output "merged_authorization_rules" { # output "_05_rules_with_first_last_decimal" { # value = local.rules_with_first_last_decimal # } -# output "_06_rules_with_everyone_meta" { -# value = local.rules_with_everyone_meta -# } -# output "_07_rules_with_everyone_duplicates_removed" { +# output "_06_rules_with_everyone_duplicates_removed" { # value = local.rules_with_everyone_duplicates_removed # } -# output "_08_rules_with_additional_cidrs" { +# output "_07_rules_with_additional_cidrs" { # value = local.rules_with_additional_cidrs # } -# output "_09_rules_with_additional_cidrs_distinct" { +# output "_08_rules_with_additional_cidrs_distinct" { # value = local.rules_with_additional_cidrs_distinct # } -# output "_10_merge_cidr_for_redundancy_check" { +# output "_09_merge_cidr_for_redundancy_check" { # value = module.merge_cidr_for_redundancy_check.merged_cidr_sets_ipv4_with_meta # } -# output "_11_rules_with_unnecessary_larger_removed" { +# output "_10_rules_with_unnecessary_larger_removed" { # value = local.rules_with_unnecessary_larger_removed # } diff --git a/tests/.terraform.lock.hcl b/tests/.terraform.lock.hcl deleted file mode 100644 index 0df4252..0000000 --- a/tests/.terraform.lock.hcl +++ /dev/null @@ -1,24 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/aws" { - version = "5.58.0" - hashes = [ - "h1:J0JP487E7DfMLr1R5s9g9HfeDaKu2E8KaZv6MlhVVi4=", - "zh:15e9be54a8febe8e560362b10967cb60b680ca3f78fe207d7209b76e076f59d3", - "zh:240f6899a2cec259aa2729ce031f6af2b453f90a8b59118bb2571c54acc65db8", - "zh:2b6e8e2ab1a3dce1001503dba6086a128bb2a71652b0d0b3b107db665b7d6881", - "zh:579b0ed95247a0bd8bfb3fac7fb767547dde76026c578f4f184b5743af5e32cc", - "zh:6adcd10fd12be0be9eb78a89e745a5b77ae0d8b3522cd782456a71178aad8ccb", - "zh:7f829cef82f0a02faa97d0fbe1417a40b73fc5142e883b12eebc5b71015efac9", - "zh:81977f001998c9096f7b59710996e159774a9313c1bc03db3beb81c3e016ebef", - "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:a5d98ac6fab6e6c85164ca7dd38f94a1e44bd70c0e8354c61f7fbabf698957cd", - "zh:c27fa4fed50f6f83ca911bef04f05d635a7b7a01a89dc8fc5d66a277588f08df", - "zh:d4042bdf86ca6dc10e0cca91c4fcc592b12572d26185b3d37bbbb9e2026ac68b", - "zh:d536482cf4ace0d49a2a86c931150921649beae59337d0c02a785879fe943cf3", - "zh:e205f8243274a621fb9ef2b5e2c71e84c1670be1d23697739439f5a831fa620f", - "zh:eb76ce0c77fd76c47f57122c91c4fcf0f72c01423538ed7833eaa7eeaae2edf6", - "zh:ffe04e494af6cc7348ceb8d85f4c1d5a847a44510827b4496513c810a4d9196d", - ] -} diff --git a/tests/debug.tf b/tests/debug.tf index d7d5ddd..0ff2103 100644 --- a/tests/debug.tf +++ b/tests/debug.tf @@ -1,81 +1,34 @@ module "vpn_authorization_rules" { source = "../" authorization_rules = [ - # { - # #description = "192.168.0.0/23 everyone" - # target_network_cidr = "192.168.0.0/23" - # authorize_all_groups = true - # }, - # { - # description = "192.168.1.0/24 dev" - # target_network_cidr = "192.168.1.0/24" - # access_group_id = "dev" - # }, - # { - # description = "192.168.1.0/24 admin" - # target_network_cidr = "192.168.1.0/24" - # access_group_id = "admin" - # }, - # { - # description = "192.168.8.0/24 admin" - # target_network_cidr = "192.168.8.0/24" - # access_group_id = "admin" - # }, - # { - # description = "192.168.8.1/32 dev" - # target_network_cidr = "192.168.8.1/32" - # access_group_id = "dev" - # }, - # { - # description = "192.168.20.0/24 admin" - # target_network_cidr = "192.168.20.0/24" - # access_group_id = "admin" - # }, - # { - # description = "192.168.20.0/25 dev" - # target_network_cidr = "192.168.20.0/25" - # access_group_id = "dev" - # }, - # { - # description = "192.168.20.128/25 dev" - # target_network_cidr = "192.168.20.128/25" - # access_group_id = "test" - # }, - - # { - # description = "0.0.0.0/0 bigadmin" - # target_network_cidr = "0.0.0.0/0" - # access_group_id = "bigadmin" - # }, - # { - # description = "0.0.0.0/1 everyone" - # authorize_all_groups = true - # target_network_cidr = "0.0.0.0/1" - # }, - # { - # description = "0.0.0.0/16 dev2" - # target_network_cidr = "0.0.0.0/16" - # access_group_id = "dev2" - # }, { - description = "1.0.0.0/16 bigadmin" + description = "Admin 1" target_network_cidr = "1.0.0.0/16" - access_group_id = "bigadmin" + access_group_id = "admin" }, { - description = "1.0.0.0/17 everyone" + description = "Everyone 1" authorize_all_groups = true target_network_cidr = "1.0.0.0/17" }, { - description = "1.0.0.0/18 dev2" + description = "Dev 1" target_network_cidr = "1.0.0.0/18" - access_group_id = "dev2" + access_group_id = "dev" + }, + { + description = "Dev 2" + target_network_cidr = "1.0.128.0/19" + access_group_id = "dev" + }, + { + description = "Dev 3" + target_network_cidr = "1.0.160.0/19" + access_group_id = "dev" }, ] - client_vpn_endpoint_id = "my-client-vpn" } -output "vpn_authorization_rules" { - value = module.vpn_authorization_rules +output "merged_authorization_rules" { + value = module.vpn_authorization_rules.merged_authorization_rules } diff --git a/tests/ipv4.tftest.hcl b/tests/ipv4.tftest.hcl deleted file mode 100644 index 330bfa1..0000000 --- a/tests/ipv4.tftest.hcl +++ /dev/null @@ -1,75 +0,0 @@ -run "set_1" { - variables { - authorization_rules = [ - # { - # description = "192.168.0.0/23 everyone" - # target_network_cidr = "192.168.0.0/23" - # authorize_all_groups = true - # }, - # { - # description = "192.168.1.0/24 dev" - # target_network_cidr = "192.168.1.0/24" - # access_group_id = "dev" - # }, - # { - # description = "192.168.1.0/24 admin" - # target_network_cidr = "192.168.1.0/24" - # access_group_id = "admin" - # }, - # { - # description = "192.168.8.0/24 admin" - # target_network_cidr = "192.168.8.0/24" - # access_group_id = "admin" - # }, - # { - # description = "192.168.8.1/32 dev" - # target_network_cidr = "192.168.8.1/32" - # access_group_id = "dev" - # }, - # { - # description = "192.168.20.0/24 admin" - # target_network_cidr = "192.168.20.0/24" - # access_group_id = "admin" - # }, - # { - # description = "192.168.20.0/25 dev" - # target_network_cidr = "192.168.20.0/25" - # access_group_id = "dev" - # }, - # { - # description = "192.168.20.128/25 dev" - # target_network_cidr = "192.168.20.128/25" - # access_group_id = "test" - # }, - { - description = "1.0.0.0/16 bigadmin" - target_network_cidr = "1.0.0.0/16" - access_group_id = "bigadmin" - }, - { - description = "1.0.0.0/15 everyone" - authorize_all_groups = true - target_network_cidr = "1.0.0.0/15" - }, - { - description = "1.0.0.0/18 dev2" - target_network_cidr = "0.0.0.0/18" - access_group_id = "dev2" - }, - ] - } - - assert { - condition = { for k, v in output.merged_authorization_rules : k => { - access_group_id = v.access_group_id - authorize_all_groups = v.authorize_all_groups - target_network_cidr = v.target_network_cidr - }} == { - "192.168.1.0/24", - "192.168.2.0/23", - "192.168.4.0/22", - "192.168.8.0/24", - ] - error_message = "Incorrect respose in set 0: ${jsonencode(output.merged_cidr_sets_ipv4.set-0)}" - } -} diff --git a/tests/main.tftest.hcl b/tests/main.tftest.hcl new file mode 100644 index 0000000..254dae8 --- /dev/null +++ b/tests/main.tftest.hcl @@ -0,0 +1,250 @@ +variables { + input_output_pairs = [ + [ + // Input + [ + { + target_network_cidr = "1.0.0.0/16" + access_group_id = "admin" + }, + { + target_network_cidr = "1.0.0.0/17" + authorize_all_groups = true + }, + { + target_network_cidr = "1.0.0.0/18" + access_group_id = "dev" + }, + ], + + // Output + { + "admin|1.0.0.0/16" = { + access_group_id = "admin" + authorize_all_groups = null + target_network_cidr = "1.0.0.0/16" + } + "eb8325b4-c0c4-4b49-962f-71a5949efb24|1.0.0.0/17" = { + access_group_id = null + authorize_all_groups = true + target_network_cidr = "1.0.0.0/17" + } + }, + ], + + [ + [ + { + target_network_cidr = "192.168.0.0/23" + authorize_all_groups = true + description = "192.168.0.0/23 everyone" + }, + { + target_network_cidr = "192.168.1.0/24" + access_group_id = "admin" + description = "192.168.1.0/24 admin" + }, + { + target_network_cidr = "192.168.8.0/24" + access_group_id = "admin" + description = "192.168.8.0/24 admin" + }, + { + target_network_cidr = "192.168.20.0/24" + access_group_id = "admin" + description = "192.168.20.0/24 admin" + }, + { + target_network_cidr = "200.100.2.0/24" + access_group_id = "admin" + description = "200.100.2.0/24 admin" + }, + { + target_network_cidr = "200.100.3.0/24" + access_group_id = "admin" + description = "200.100.3.0/24 admin" + }, + { + target_network_cidr = "192.168.8.1/32" + access_group_id = "dev" + description = "192.168.8.1/32 dev" + }, + { + target_network_cidr = "192.168.1.0/24" + access_group_id = "dev" + description = "192.168.1.0/24 dev" + }, + { + target_network_cidr = "192.168.20.0/25" + access_group_id = "dev" + description = "192.168.20.0/25 dev" + }, + { + target_network_cidr = "192.168.20.128/25" + access_group_id = "test" + description = "192.168.20.128/25 dev" + }, + { + target_network_cidr = "0.0.0.0/16" + access_group_id = "testers" + description = "0.0.0.0/16 testers" + }, + { + target_network_cidr = "0.0.0.0/0" + access_group_id = "superadmin" + description = "0.0.0.0/0 superadmin" + }, + { + target_network_cidr = "0.0.0.0/1" + authorize_all_groups = true + description = "0.0.0.0/1 everyone" + }, + ], + { + "admin|192.168.20.0/25" = { + access_group_id = "admin" + authorize_all_groups = null + description = "192.168.20.0/24 admin (covering longest prefix path from \"192.168.20.0/25 dev\")" + target_network_cidr = "192.168.20.0/25" + } + "admin|192.168.20.128/25" = { + access_group_id = "admin" + authorize_all_groups = null + description = "192.168.20.0/24 admin (covering longest prefix path from \"192.168.20.128/25 dev\")" + target_network_cidr = "192.168.20.128/25" + } + "admin|192.168.8.0/24" = { + access_group_id = "admin" + authorize_all_groups = null + description = "192.168.8.0/24 admin" + target_network_cidr = "192.168.8.0/24" + } + "admin|192.168.8.1/32" = { + access_group_id = "admin" + authorize_all_groups = null + description = "192.168.8.0/24 admin (covering longest prefix path from \"192.168.8.1/32 dev\")" + target_network_cidr = "192.168.8.1/32" + } + "admin|200.100.2.0/23" = { + access_group_id = "admin" + authorize_all_groups = null + description = "200.100.2.0/24 admin; 200.100.3.0/24 admin" + target_network_cidr = "200.100.2.0/23" + } + "dev|192.168.20.0/25" = { + access_group_id = "dev" + authorize_all_groups = null + description = "192.168.20.0/25 dev" + target_network_cidr = "192.168.20.0/25" + } + "dev|192.168.8.1/32" = { + access_group_id = "dev" + authorize_all_groups = null + description = "192.168.8.1/32 dev" + target_network_cidr = "192.168.8.1/32" + } + "eb8325b4-c0c4-4b49-962f-71a5949efb24|0.0.0.0/1" = { + access_group_id = null + authorize_all_groups = true + description = "0.0.0.0/1 everyone" + target_network_cidr = "0.0.0.0/1" + } + "eb8325b4-c0c4-4b49-962f-71a5949efb24|192.168.0.0/23" = { + access_group_id = null + authorize_all_groups = true + description = "192.168.0.0/23 everyone" + target_network_cidr = "192.168.0.0/23" + } + "superadmin|0.0.0.0/0" = { + access_group_id = "superadmin" + authorize_all_groups = null + description = "0.0.0.0/0 superadmin" + target_network_cidr = "0.0.0.0/0" + } + "superadmin|192.168.20.0/24" = { + access_group_id = "superadmin" + authorize_all_groups = null + description = "0.0.0.0/0 superadmin (covering longest prefix path from \"192.168.20.0/24 admin\")" + target_network_cidr = "192.168.20.0/24" + } + "superadmin|192.168.20.0/25" = { + access_group_id = "superadmin" + authorize_all_groups = null + description = "0.0.0.0/0 superadmin (covering longest prefix path from \"192.168.20.0/25 dev\")" + target_network_cidr = "192.168.20.0/25" + } + "superadmin|192.168.20.128/25" = { + access_group_id = "superadmin" + authorize_all_groups = null + description = "0.0.0.0/0 superadmin (covering longest prefix path from \"192.168.20.128/25 dev\")" + target_network_cidr = "192.168.20.128/25" + } + "superadmin|192.168.8.0/24" = { + access_group_id = "superadmin" + authorize_all_groups = null + description = "0.0.0.0/0 superadmin (covering longest prefix path from \"192.168.8.0/24 admin\")" + target_network_cidr = "192.168.8.0/24" + } + "superadmin|192.168.8.1/32" = { + access_group_id = "superadmin" + authorize_all_groups = null + description = "0.0.0.0/0 superadmin (covering longest prefix path from \"192.168.8.1/32 dev\")" + target_network_cidr = "192.168.8.1/32" + } + "superadmin|200.100.2.0/23" = { + access_group_id = "superadmin" + authorize_all_groups = null + description = "0.0.0.0/0 superadmin (covering longest prefix path from \"200.100.2.0/24 admin; 200.100.3.0/24 admin\")" + target_network_cidr = "200.100.2.0/23" + } + "test|192.168.20.128/25" = { + access_group_id = "test" + authorize_all_groups = null + description = "192.168.20.128/25 dev" + target_network_cidr = "192.168.20.128/25" + } + } + ] + ] +} + +run "execute_0" { + variables { + authorization_rules = var.input_output_pairs[0][0] + } +} + +run "assert_0" { + module { + source = "./tests/result-comparison" + } + variables { + expected_rules = var.input_output_pairs[0][1] + actual_rules = run.execute_0.merged_authorization_rules + } + + assert { + condition = length(output.mismatched_rules) == 0 + error_message = "The following rules were mismatched:\n${jsonencode(output.mismatched_rules)}" + } +} + +run "execute_1" { + variables { + authorization_rules = var.input_output_pairs[1][0] + } +} +run "assert_1" { + module { + source = "./tests/result-comparison" + } + variables { + expected_rules = var.input_output_pairs[1][1] + actual_rules = run.execute_1.merged_authorization_rules + } + + assert { + condition = length(output.mismatched_rules) == 0 + error_message = "The following rules were mismatched:\n${jsonencode(output.mismatched_rules)}" + } +} diff --git a/tests/result-comparison/main.tf b/tests/result-comparison/main.tf new file mode 100644 index 0000000..335cc43 --- /dev/null +++ b/tests/result-comparison/main.tf @@ -0,0 +1,44 @@ +variable "expected_rules" {} +variable "actual_rules" {} + +locals { + rule_comparison = merge({ + for key, expected in var.expected_rules : + key => { + unexpected = false + found = lookup(var.actual_rules, key, null) != null + access_group_id = lookup(var.actual_rules, key, null) == null ? null : { + expected = expected.access_group_id + actual = var.actual_rules[key].access_group_id + } + target_network_cidr = lookup(var.actual_rules, key, null) == null ? null : { + expected = expected.target_network_cidr + actual = var.actual_rules[key].target_network_cidr + } + } + }, { + for key, unexpected in var.actual_rules : + key => { + unexpected = true + found = true + access_group_id = null + target_network_cidr = null + } + if lookup(var.expected_rules, key, null) == null + }) + + mismatched_rules = { + for key, rule in local.rule_comparison : + key => rule + if(rule.unexpected ? true : ( + !rule.found ? true : (rule.access_group_id.expected != rule.access_group_id.actual) ? true : ( + rule.target_network_cidr.expected != rule.target_network_cidr.actual + ) + ) + ) + } +} + +output "mismatched_rules" { + value = local.mismatched_rules +} diff --git a/variables.tf b/variables.tf index 8ca8702..2a3cbc5 100644 --- a/variables.tf +++ b/variables.tf @@ -41,91 +41,19 @@ variable "authorization_rules" { condition = length([ for idx, rule in var.authorization_rules : idx - if( - !can(cidrhost(rule.target_network_cidr, 0)) - # // Ensure there's one "/" - # length(split("/", rule.target_network_cidr)) != 2 || - # // Ensure the IP part has 4 segments - # length(split(".", split("/", rule.target_network_cidr)[0])) != 4 || - # // Ensure the prefix length part is an integer - # length(regexall("^[0-9]{1,2}$", split("/", rule.target_network_cidr)[1])) == 0 || - # // Ensure the prefix length is a valid value - # tonumber(split("/", rule.target_network_cidr)[1]) < 0 || - # tonumber(split("/", rule.target_network_cidr)[1]) > 32 || - # // Ensure the first octet is a valid value - # length(regexall("^[0-9]{1,3}$", split(".", split("/", rule.target_network_cidr)[0])[0])) == 0 || - # tonumber(split(".", split("/", rule.target_network_cidr)[0])[0]) < 0 || - # tonumber(split(".", split("/", rule.target_network_cidr)[0])[0]) > 255 || - # // Ensure the second octet is a valid value - # length(regexall("^[0-9]{1,3}$", split(".", split("/", rule.target_network_cidr)[0])[1])) == 0 || - # tonumber(split(".", split("/", rule.target_network_cidr)[0])[1]) < 0 || - # tonumber(split(".", split("/", rule.target_network_cidr)[0])[1]) > 255 || - # // Ensure the third octet is a valid value - # length(regexall("^[0-9]{1,3}$", split(".", split("/", rule.target_network_cidr)[0])[2])) == 0 || - # tonumber(split(".", split("/", rule.target_network_cidr)[0])[2]) < 0 || - # tonumber(split(".", split("/", rule.target_network_cidr)[0])[2]) > 255 || - # // Ensure the fourth octet is a valid value - # length(regexall("^[0-9]{1,3}$", split(".", split("/", rule.target_network_cidr)[0])[3])) == 0 || - # tonumber(split(".", split("/", rule.target_network_cidr)[0])[3]) < 0 || - # tonumber(split(".", split("/", rule.target_network_cidr)[0])[3]) > 255 - ) + if !can(cidrhost(rule.target_network_cidr, 0)) ]) == 0 error_message = "Authorization rules at indecies [${join(", ", [ for idx, rule in var.authorization_rules : idx - if( - !can(cidrhost(rule.target_network_cidr, 0)) - # // Ensure there's one "/" - # length(split("/", rule.target_network_cidr)) != 2 || - # // Ensure the IP part has 4 segments - # length(split(".", split("/", rule.target_network_cidr)[0])) != 4 || - # // Ensure the prefix length part is an integer - # length(regexall("^[0-9]{1,2}$", split("/", rule.target_network_cidr)[1])) == 0 || - # // Ensure the prefix length is a valid value - # tonumber(split("/", rule.target_network_cidr)[1]) < 0 || - # tonumber(split("/", rule.target_network_cidr)[1]) > 32 || - # // Ensure the first octet is a valid value - # length(regexall("^[0-9]{1,3}$", split(".", split("/", rule.target_network_cidr)[0])[0])) == 0 || - # tonumber(split(".", split("/", rule.target_network_cidr)[0])[0]) < 0 || - # tonumber(split(".", split("/", rule.target_network_cidr)[0])[0]) > 255 || - # // Ensure the second octet is a valid value - # length(regexall("^[0-9]{1,3}$", split(".", split("/", rule.target_network_cidr)[0])[1])) == 0 || - # tonumber(split(".", split("/", rule.target_network_cidr)[0])[1]) < 0 || - # tonumber(split(".", split("/", rule.target_network_cidr)[0])[1]) > 255 || - # // Ensure the third octet is a valid value - # length(regexall("^[0-9]{1,3}$", split(".", split("/", rule.target_network_cidr)[0])[2])) == 0 || - # tonumber(split(".", split("/", rule.target_network_cidr)[0])[2]) < 0 || - # tonumber(split(".", split("/", rule.target_network_cidr)[0])[2]) > 255 || - # // Ensure the fourth octet is a valid value - # length(regexall("^[0-9]{1,3}$", split(".", split("/", rule.target_network_cidr)[0])[3])) == 0 || - # tonumber(split(".", split("/", rule.target_network_cidr)[0])[3]) < 0 || - # tonumber(split(".", split("/", rule.target_network_cidr)[0])[3]) > 255 - ) + if !can(cidrhost(rule.target_network_cidr, 0)) ])}] have invalid CIDR blocks in the `target_network_cidr` field." } } -variable "client_vpn_endpoint_id" { - description = "The ID of the Client VPN endpoint to associate the authorization rules with." - type = string - nullable = true -} - variable "merged_rule_description_joiner" { description = "The string to use for joining authorization rule descriptions when multiple rules are merged into one CIDR." type = string default = "; " nullable = false } - -variable "create_rules" { - description = "Whether to actually create the Client VPN authorization rules. If `false`, the output `merged_authorization_rules` can be used by the user to create the rules separately." - type = bool - default = false - nullable = false - - validation { - condition = var.create_rules ? var.client_vpn_endpoint_id != null : true - error_message = "If the `create_rules` variable is `true`, then `client_vpn_endpoint_id` must be provided." - } -} diff --git a/versions.tf b/versions.tf index f92c54b..50d01f3 100644 --- a/versions.tf +++ b/versions.tf @@ -1,9 +1,3 @@ terraform { required_version = ">= 1.9" - required_providers { - aws = { - source = "hashicorp/aws" - version = ">= 5.58.0" - } - } }