From f4ebecaf85916391d4b678fd7b84e61bb664296c Mon Sep 17 00:00:00 2001 From: Kyle Kotowick Date: Thu, 18 Jul 2024 11:33:46 -0400 Subject: [PATCH] Initial code --- LICENSE | 21 +++ main.tf | 268 ++++++++++++++++++++++++++++++++++++++ outputs.tf | 64 +++++++++ tests/.terraform.lock.hcl | 24 ++++ tests/debug.tf | 81 ++++++++++++ tests/ipv4.tftest.hcl | 75 +++++++++++ variables.tf | 131 +++++++++++++++++++ versions.tf | 9 ++ 8 files changed, 673 insertions(+) create mode 100644 LICENSE create mode 100644 main.tf create mode 100644 outputs.tf create mode 100644 tests/.terraform.lock.hcl create mode 100644 tests/debug.tf create mode 100644 tests/ipv4.tftest.hcl create mode 100644 variables.tf create mode 100644 versions.tf diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8b4fbdf --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Invicton Labs (https://invictonlabs.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..d2e915c --- /dev/null +++ b/main.tf @@ -0,0 +1,268 @@ +locals { + // We need a unique key for the "everyone" (all users) rules, + // so we use a UUIDv4 for it that should never appear as a group ID. + // Unless, of course, you look at this module and try to break it by + // purposefully using this UUID as a group ID. But you wouldn't do that... + everyone_group_uuid = "eb8325b4-c0c4-4b49-962f-71a5949efb24" + + // All rules, with the everyone group UUID added for all-user rules + all_rules_initial = { + for key, value in var.authorization_rules : + key => value.authorize_all_groups ? merge(value, { + access_group_id = local.everyone_group_uuid + }) : value + } + + // A list of all group IDs that are used in any rule + all_group_ids = distinct([ + for rule in local.all_rules_initial : + rule.access_group_id + ]) + + // Categorize the rules by group + rules_by_group = { + for group_id in local.all_group_ids : + (group_id == null ? tonumber(0) : tostring(group_id)) => { + for key, value in local.all_rules_initial : + key => value.description == "" ? merge(value, { + description = "${value.authorize_all_groups ? "ALL_USERS" : value.access_group_id}:${value.target_network_cidr}" + }) : value + if value.access_group_id == group_id + } + } +} + +/* +For each rule: +1. If there's a larger rule for the same group, delete the smaller one (done with CIDR merging module) +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) +*/ + +// Start by reducing all rules within each group to the minimum set. +module "cidr_merge" { + source = "Invicton-Labs/merge-cidrs/null" + version = "~>0.1.0" + cidr_sets_ipv4 = { + for group_id, rules in local.rules_by_group : + group_id => [ + for rule in rules : + { + cidr = rule.target_network_cidr + metadata = { + description = rule.description + } + } + ] + } +} + +locals { + // Use our merged rules to create a new set of rules for each group, with additional metadata. + merged_rules = { + for group_id, cidr_datas in module.cidr_merge.merged_cidr_sets_ipv4_with_meta : + group_id => [ + for cidr_data in cidr_datas : + { + group_id = group_id + cidr = cidr_data.cidr + description = join(var.merged_rule_description_joiner, [for contained in cidr_data.contains : contained.metadata.description]) + first_ip = cidrhost(cidr_data.cidr, 0) + last_ip = cidrhost(cidr_data.cidr, pow(2, 32 - tonumber(split("/", cidr_data.cidr)[1])) - 1) + } + ] + } + + // Add decimal conversions of the first and last IPs for each rule. + rules_with_first_last_decimal = { + for group_id, rules in local.merged_rules : + group_id => [ + for rule in rules : + merge(rule, { + first_ip_decimal = pow(2, 24) * tonumber(split(".", rule.first_ip)[0]) + pow(2, 16) * tonumber(split(".", rule.first_ip)[1]) + pow(2, 8) * tonumber(split(".", rule.first_ip)[2]) + tonumber(split(".", rule.first_ip)[3]) + last_ip_decimal = pow(2, 24) * tonumber(split(".", rule.last_ip)[0]) + pow(2, 16) * tonumber(split(".", rule.last_ip)[1]) + pow(2, 8) * tonumber(split(".", rule.last_ip)[2]) + tonumber(split(".", rule.last_ip)[3]) + }) + ] + } + + // 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 = { + 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 + ) + ] + } + + // For each rule, add additional rules to match any longer-prefix rules for other groups. + rules_with_additional_cidrs = { + for group_id, rules in local.rules_with_everyone_duplicates_removed : + group_id => flatten([ + for rule in rules : + concat([ + merge(rule, { + extra_due_to_other_group = false + }) + ], [ + for compare_rule in local.all_rules_with_everyone_meta : + merge(compare_rule, { + description = "${rule.description} (covering longest 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) + ) + ]) + ]) + } + + // For each group, eliminate any duplicate rules by only taking the first rule for each distinct CIDR. + // Duplicates may exist if the same extra rule was added once each for multiple other groups. + rules_with_additional_cidrs_distinct = { + for group_id, rules in local.rules_with_additional_cidrs : + group_id => [ + for cidr in distinct([for rule in rules : rule.cidr]) : + [ + for rule in rules : + rule + if rule.cidr == cidr + ][0] + ] + } +} + +// For each group, merge all of the rules that are required for longer prefixes in other groups. +module "merge_cidr_for_redundancy_check" { + source = "Invicton-Labs/merge-cidrs/null" + version = "~>0.1.0" + cidr_sets_ipv4 = { + for group_id, rules in local.rules_with_additional_cidrs_distinct : + group_id => [ + for rule in rules : + { + cidr = rule.cidr + } + if rule.extra_due_to_other_group + ] + } +} + +locals { + // Remove any rules that weren't added due to longer prefixes existing in other groups, + // and that are redundant because the entire range is covered by smaller groups that WERE + // added due to longer prefixes existing in other groups. + rules_with_unnecessary_larger_removed = { + for group_id, rules in local.rules_with_additional_cidrs_distinct : + group_id => [ + for rule in rules : + 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 + ) + ) + ] + } + + // The full set of rules, formatted with the fields expected by aws_ec2_client_vpn_authorization_rule + all_rules = { + for rule in flatten(values(local.rules_with_unnecessary_larger_removed)) : + "${rule.group_id}|${rule.cidr}" => { + target_network_cidr = rule.cidr + authorize_all_groups = rule.group_id == local.everyone_group_uuid ? true : null + access_group_id = rule.group_id == local.everyone_group_uuid ? null : rule.group_id + 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 new file mode 100644 index 0000000..a0b3c85 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,64 @@ +//================================================== +// Outputs that match the input variables +//================================================== +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 +} + +//================================================== +// Outputs generated by this module +//================================================== +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 +//================================================== +# output "_01_all_rules_initial" { +# value = local.all_rules_initial +# } +# output "_02_all_group_ids" { +# value = local.all_group_ids +# } +# output "_03_rules_by_group" { +# value = local.rules_by_group +# } +# output "_04_merged_rules" { +# value = local.merged_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" { +# value = local.rules_with_everyone_duplicates_removed +# } +# output "_08_rules_with_additional_cidrs" { +# value = local.rules_with_additional_cidrs +# } +# output "_09_rules_with_additional_cidrs_distinct" { +# value = local.rules_with_additional_cidrs_distinct +# } +# output "_10_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" { +# value = local.rules_with_unnecessary_larger_removed +# } diff --git a/tests/.terraform.lock.hcl b/tests/.terraform.lock.hcl new file mode 100644 index 0000000..0df4252 --- /dev/null +++ b/tests/.terraform.lock.hcl @@ -0,0 +1,24 @@ +# 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 new file mode 100644 index 0000000..d7d5ddd --- /dev/null +++ b/tests/debug.tf @@ -0,0 +1,81 @@ +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" + target_network_cidr = "1.0.0.0/16" + access_group_id = "bigadmin" + }, + { + description = "1.0.0.0/17 everyone" + authorize_all_groups = true + target_network_cidr = "1.0.0.0/17" + }, + { + description = "1.0.0.0/18 dev2" + target_network_cidr = "1.0.0.0/18" + access_group_id = "dev2" + }, + ] + client_vpn_endpoint_id = "my-client-vpn" +} + +output "vpn_authorization_rules" { + value = module.vpn_authorization_rules +} diff --git a/tests/ipv4.tftest.hcl b/tests/ipv4.tftest.hcl new file mode 100644 index 0000000..330bfa1 --- /dev/null +++ b/tests/ipv4.tftest.hcl @@ -0,0 +1,75 @@ +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/variables.tf b/variables.tf new file mode 100644 index 0000000..8ca8702 --- /dev/null +++ b/variables.tf @@ -0,0 +1,131 @@ +variable "authorization_rules" { + description = "A list of Client VPN subnet access authorizations." + type = list(object({ + description = optional(string, "") + target_network_cidr = string + access_group_id = optional(string) + authorize_all_groups = optional(bool, false) + })) + nullable = false + + // Ensure there are no rules with neither the access group ID nor the all groups set + validation { + condition = length([ + for idx, rule in var.authorization_rules : + idx + if rule.access_group_id == null && (rule.authorize_all_groups == null || rule.authorize_all_groups == false) + ]) == 0 + error_message = "Authorization rules at indecies [${join(", ", [ + for idx, rule in var.authorization_rules : + idx + if rule.access_group_id == null && (rule.authorize_all_groups == null || rule.authorize_all_groups == false) + ])}] must either provide an `access_group_id` or have `authorize_all_groups` set to `true`." + } + + // Ensure there are no rules with both the access group ID and all groups set + validation { + condition = length([ + for idx, rule in var.authorization_rules : + idx + if rule.access_group_id != null && rule.authorize_all_groups == true + ]) == 0 + error_message = "Authorization rules at indecies [${join(", ", [ + for idx, rule in var.authorization_rules : + idx + if rule.access_group_id != null && rule.authorize_all_groups == true + ])}] have both an `access_group_id` provided and `authorize_all_groups` set to `true`, but these are mutually exclusive." + } + + // Ensure all CIDR targets are valid + validation { + 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 + ) + ]) == 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 + ) + ])}] 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 new file mode 100644 index 0000000..f92c54b --- /dev/null +++ b/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.9" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.58.0" + } + } +}