From 187e29d068bcbe26ab7c05e0607ad93a12179b1c Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sun, 30 Aug 2026 00:58:59 +1000 Subject: [PATCH] Validate user and role names instead of munging binding names Rancher requires global role binding names to be RFC 1123 labels. The per-user binding name lowercased the key and replaced only "/", so usernames containing ".", "_" or "@" still produced an invalid name and failed at apply, and hyphenated names could collide (foo-bar + baz vs foo + bar-baz) into one object. Add validation blocks on var.users requiring the username and every referenced role name to be RFC 1123 labels, so non-compliant input fails the plan with an actionable message. The binding name is then built from the two parts directly. A precondition rejects the remaining hyphen ambiguity at plan time rather than as a mid-apply conflict. Document the constraint in the users schema section of the README. The group binding path is unchanged. --- README.md | 9 +++++++++ modules/rancher/main.tf | 19 ++++++++++++++++--- modules/rancher/variables.tf | 22 ++++++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 479bad9..1ebe5bc 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,15 @@ global_role_bindings: # optional The password is read from Vault **at plan time**, so seed the kv-v2 secret before adding the file, or the plan fails. +The file name (the username) and every entry in `global_role_bindings` must be +an RFC 1123 label — `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`, i.e. lowercase +alphanumerics and `-`, starting and ending alphanumeric. No `.`, `_`, `@` or +uppercase. Rancher names each binding `akuser--` and rejects +anything else, so non-compliant values fail the plan with an explicit error +instead of the apply. Names that are individually valid but collapse to the same +binding name (e.g. `foo-bar` + `baz` and `foo` + `bar-baz`) are rejected at plan +time too. + ### `config/roles/.yaml` ```yaml diff --git a/modules/rancher/main.tf b/modules/rancher/main.tf index 9002a32..af9e45d 100644 --- a/modules/rancher/main.tf +++ b/modules/rancher/main.tf @@ -90,11 +90,24 @@ locals { resource "rancher2_global_role_binding" "user" { for_each = local.user_global_role_bindings - # Binding name must be an RFC 1123 label, so lowercase it and swap the key's - # "/" separator for "-". - name = "akuser-${lower(replace(each.key, "/", "-"))}" + # Both parts are RFC 1123 labels already (var.users validates them), so the + # binding name needs no transformation. + name = "akuser-${each.value.username}-${each.value.global_role_id}" global_role_id = each.value.global_role_id user_id = rancher2_user.this[each.value.username].id + + lifecycle { + # Hyphens are legal inside both parts, so distinct bindings can still flatten + # to the same name (foo-bar + baz vs foo + bar-baz). Catch that at plan time + # rather than as a duplicate-object conflict mid-apply. + precondition { + condition = length(distinct([ + for binding in local.user_global_role_bindings : + "akuser-${binding.username}-${binding.global_role_id}" + ])) == length(local.user_global_role_bindings) + error_message = "Two user global role bindings collapse to the same Rancher binding name (akuser--). Rename one of the users or roles involved." + } + } } # Custom global roles. Rules are optional: a role with none grants nothing on its diff --git a/modules/rancher/variables.tf b/modules/rancher/variables.tf index fe359de..6713a26 100644 --- a/modules/rancher/variables.tf +++ b/modules/rancher/variables.tf @@ -51,6 +51,28 @@ variable "users" { global_role_bindings = optional(list(string), []) })) default = {} + + # Rancher names each global role binding as an RFC 1123 label and rejects + # anything else at apply time. The binding name is built from the username and + # the role name verbatim, so both must already be compliant — fail the plan + # with a clear message instead of munging the input and hoping. + validation { + condition = alltrue([ + for username in keys(var.users) : + can(regex("^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", username)) + ]) + error_message = "Usernames must be RFC 1123 labels: lowercase alphanumerics and '-', starting and ending alphanumeric (no '.', '_', '@' or uppercase). Rename config/users/.yaml to a compliant username." + } + + validation { + condition = alltrue(flatten([ + for user in values(var.users) : [ + for role in user.global_role_bindings : + can(regex("^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", role)) + ] + ])) + error_message = "Values in global_role_bindings must be RFC 1123 labels: lowercase alphanumerics and '-', starting and ending alphanumeric." + } } # Custom Rancher global roles (config/roles/.yaml). Map key is the role