From 1dab2ecc6fd9049f6e279114948d4919f6674157 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sat, 18 Jul 2026 16:11:03 +1000 Subject: [PATCH 1/2] Add two-tier RBAC: permission/role groups, access policies, group claim Introduce a user -> role -> [permissions] model for app access and roles, managed declaratively. - Permission groups (akP--) under config/permissions/: atomic units, each names the application it grants access to. - Role groups (akR-) under config/roles/: what users are assigned to; each nests permission groups via parents (akR-global-admin -> all *-admin, akR-standard-user -> all *-user). Split into a separate authentik_group resource so roles can reference permission ids without self-reference. - Policy bindings gate each application to its permission groups (and, via child->parent membership propagation, the roles that nest them). - Hierarchical `groups` scope mapping: walks user groups up through .parents so the OIDC claim includes inherited permission groups (works around goauthentik/authentik#15579). Inert until a provider requests the `groups` scope, so no behaviour change to existing apps until they opt in. Validated with `tofu validate`. --- config/config.hcl | 10 +++ config/permissions/ak-p-argocd-admin.yaml | 4 + config/permissions/ak-p-argocd-user.yaml | 4 + config/permissions/ak-p-grafana-admin.yaml | 4 + config/permissions/ak-p-grafana-user.yaml | 4 + config/permissions/ak-p-rancher-admin.yaml | 4 + config/permissions/ak-p-rancher-user.yaml | 4 + config/roles/ak-r-global-admin.yaml | 7 ++ config/roles/ak-r-standard-user.yaml | 6 ++ .../terragrunt.hcl | 10 ++- modules/authentik/main.tf | 73 +++++++++++++++++-- modules/authentik/variables.tf | 28 +++++++ 12 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 config/permissions/ak-p-argocd-admin.yaml create mode 100644 config/permissions/ak-p-argocd-user.yaml create mode 100644 config/permissions/ak-p-grafana-admin.yaml create mode 100644 config/permissions/ak-p-grafana-user.yaml create mode 100644 config/permissions/ak-p-rancher-admin.yaml create mode 100644 config/permissions/ak-p-rancher-user.yaml create mode 100644 config/roles/ak-r-global-admin.yaml create mode 100644 config/roles/ak-r-standard-user.yaml diff --git a/config/config.hcl b/config/config.hcl index 97eeed2..7462c05 100644 --- a/config/config.hcl +++ b/config/config.hcl @@ -12,6 +12,16 @@ locals { trimsuffix(basename(file_path), ".yaml") => content if startswith(file_path, "groups/") } + permission_groups = { + for file_path, content in local.all_configs : + trimsuffix(basename(file_path), ".yaml") => content + if startswith(file_path, "permissions/") + } + role_groups = { + for file_path, content in local.all_configs : + trimsuffix(basename(file_path), ".yaml") => content + if startswith(file_path, "roles/") + } providers_saml = { for file_path, content in local.all_configs : trimsuffix(basename(file_path), ".yaml") => content diff --git a/config/permissions/ak-p-argocd-admin.yaml b/config/permissions/ak-p-argocd-admin.yaml new file mode 100644 index 0000000..22f7078 --- /dev/null +++ b/config/permissions/ak-p-argocd-admin.yaml @@ -0,0 +1,4 @@ +# Permission: admin access to argocd. Bound to the argocd application for +# access, and mapped to the argocd admin role via the groups claim. +name: akP-argocd-admin +application: argocd diff --git a/config/permissions/ak-p-argocd-user.yaml b/config/permissions/ak-p-argocd-user.yaml new file mode 100644 index 0000000..b369880 --- /dev/null +++ b/config/permissions/ak-p-argocd-user.yaml @@ -0,0 +1,4 @@ +# Permission: user access to argocd. Bound to the argocd application for +# access, and mapped to the argocd user role via the groups claim. +name: akP-argocd-user +application: argocd diff --git a/config/permissions/ak-p-grafana-admin.yaml b/config/permissions/ak-p-grafana-admin.yaml new file mode 100644 index 0000000..955e29a --- /dev/null +++ b/config/permissions/ak-p-grafana-admin.yaml @@ -0,0 +1,4 @@ +# Permission: admin access to grafana. Bound to the grafana application for +# access, and mapped to the grafana admin role via the groups claim. +name: akP-grafana-admin +application: grafana diff --git a/config/permissions/ak-p-grafana-user.yaml b/config/permissions/ak-p-grafana-user.yaml new file mode 100644 index 0000000..2092784 --- /dev/null +++ b/config/permissions/ak-p-grafana-user.yaml @@ -0,0 +1,4 @@ +# Permission: user access to grafana. Bound to the grafana application for +# access, and mapped to the grafana user role via the groups claim. +name: akP-grafana-user +application: grafana diff --git a/config/permissions/ak-p-rancher-admin.yaml b/config/permissions/ak-p-rancher-admin.yaml new file mode 100644 index 0000000..1acc5e5 --- /dev/null +++ b/config/permissions/ak-p-rancher-admin.yaml @@ -0,0 +1,4 @@ +# Permission: admin access to rancher. Bound to the rancher application for +# access, and mapped to the rancher admin role via the groups claim. +name: akP-rancher-admin +application: rancher diff --git a/config/permissions/ak-p-rancher-user.yaml b/config/permissions/ak-p-rancher-user.yaml new file mode 100644 index 0000000..d596373 --- /dev/null +++ b/config/permissions/ak-p-rancher-user.yaml @@ -0,0 +1,4 @@ +# Permission: user access to rancher. Bound to the rancher application for +# access, and mapped to the rancher user role via the groups claim. +name: akP-rancher-user +application: rancher diff --git a/config/roles/ak-r-global-admin.yaml b/config/roles/ak-r-global-admin.yaml new file mode 100644 index 0000000..d9d0a7a --- /dev/null +++ b/config/roles/ak-r-global-admin.yaml @@ -0,0 +1,7 @@ +# Role: full admin across all onboarded apps. Assign users here for org-wide admin. +name: akR-global-admin +is_superuser: false +permissions: + - ak-p-grafana-admin + - ak-p-argocd-admin + - ak-p-rancher-admin diff --git a/config/roles/ak-r-standard-user.yaml b/config/roles/ak-r-standard-user.yaml new file mode 100644 index 0000000..8e21d35 --- /dev/null +++ b/config/roles/ak-r-standard-user.yaml @@ -0,0 +1,6 @@ +# Role: standard (non-admin) access across all onboarded apps. +name: akR-standard-user +permissions: + - ak-p-grafana-user + - ak-p-argocd-user + - ak-p-rancher-user diff --git a/environments/identity.k8s.syd1.au.unkin.net/terragrunt.hcl b/environments/identity.k8s.syd1.au.unkin.net/terragrunt.hcl index e5d25b3..c85bbe7 100644 --- a/environments/identity.k8s.syd1.au.unkin.net/terragrunt.hcl +++ b/environments/identity.k8s.syd1.au.unkin.net/terragrunt.hcl @@ -17,8 +17,10 @@ terraform { } inputs = { - groups = local.config.groups - providers_saml = local.config.providers_saml - providers_oauth2 = local.config.providers_oauth2 - providers_ldap = local.config.providers_ldap + groups = local.config.groups + permission_groups = local.config.permission_groups + role_groups = local.config.role_groups + providers_saml = local.config.providers_saml + providers_oauth2 = local.config.providers_oauth2 + providers_ldap = local.config.providers_ldap } diff --git a/modules/authentik/main.tf b/modules/authentik/main.tf index 2ab3da6..69f6e8f 100644 --- a/modules/authentik/main.tf +++ b/modules/authentik/main.tf @@ -7,6 +7,50 @@ resource "authentik_group" "this" { attributes = jsonencode(each.value.attributes) } +# Permission groups (akP-*): atomic, leaf groups. Mapped to app roles via the +# groups claim and bound to applications for access. +resource "authentik_group" "permission" { + for_each = var.permission_groups + + name = each.value.name + attributes = jsonencode(each.value.attributes) +} + +# Role groups (akR-*): what users are assigned to. Each nests permission groups +# as parents, so a role member is an effective member of every permission it +# grants. Separate resource from permissions so this reference is not a +# self-reference (authentik_group cannot refer to itself). +resource "authentik_group" "role" { + for_each = var.role_groups + + name = each.value.name + is_superuser = each.value.is_superuser + parents = [for p in each.value.permissions : authentik_group.permission[p].id] + attributes = jsonencode(each.value.attributes) +} + +# Expand the OIDC `groups` claim to include inherited (ancestor) groups. The +# default profile mapping only emits direct groups (goauthentik/authentik#15579), +# so a member of a role group would not see the permission groups it nests. This +# walks each of the user's direct groups up through `.parents` and emits the full +# set of names, so role -> permission nesting drives in-app roles. Only evaluated +# when a provider requests the `groups` scope. +resource "authentik_property_mapping_provider_scope" "groups_hierarchical" { + name = "unkin: groups (hierarchical)" + scope_name = "groups" + expression = <<-EOT + groups = {} + pending = list(user.ak_groups.all()) + while pending: + grp = pending.pop() + if grp.pk in groups: + continue + groups[grp.pk] = grp.name + pending += list(grp.parents.all()) + return {"groups": sorted(groups.values())} + EOT +} + resource "authentik_provider_saml" "this" { for_each = var.providers_saml @@ -46,13 +90,16 @@ data "vault_kv_secret_v2" "oauth2" { resource "authentik_provider_oauth2" "this" { for_each = var.providers_oauth2 - name = each.value.name - authorization_flow = data.authentik_flow.oauth2_authorization[each.key].id - invalidation_flow = data.authentik_flow.oauth2_invalidation[each.key].id - client_type = each.value.client_type - client_id = each.value.client_id - client_secret = each.value.client_secret_vault != null ? data.vault_kv_secret_v2.oauth2[each.key].data["client_secret"] : null - property_mappings = try(data.authentik_property_mapping_provider_scope.oauth2[each.key].ids, []) + name = each.value.name + authorization_flow = data.authentik_flow.oauth2_authorization[each.key].id + invalidation_flow = data.authentik_flow.oauth2_invalidation[each.key].id + client_type = each.value.client_type + client_id = each.value.client_id + client_secret = each.value.client_secret_vault != null ? data.vault_kv_secret_v2.oauth2[each.key].data["client_secret"] : null + property_mappings = concat( + try(data.authentik_property_mapping_provider_scope.oauth2[each.key].ids, []), + [authentik_property_mapping_provider_scope.groups_hierarchical.id], + ) signing_key = each.value.signing_key access_token_validity = each.value.access_token_validity allowed_redirect_uris = each.value.redirect_uris @@ -105,3 +152,15 @@ resource "authentik_outpost" "ldap" { type = "ldap" protocol_providers = [authentik_provider_ldap.this[each.key].id] } + +# Gate application access: bind each permission group that names an `application` +# to that app. Authentik ORs bindings, and membership propagates from child +# groups, so a member of any role that nests the permission is also covered. +# With any binding present, only these groups (and their children) can authorize. +resource "authentik_policy_binding" "app_access" { + for_each = { for k, v in var.permission_groups : k => v if v.application != null } + + target = authentik_application.oauth2[each.value.application].uuid + group = authentik_group.permission[each.key].id + order = 0 +} diff --git a/modules/authentik/variables.tf b/modules/authentik/variables.tf index d2f5d0c..86bc700 100644 --- a/modules/authentik/variables.tf +++ b/modules/authentik/variables.tf @@ -10,6 +10,34 @@ variable "groups" { default = {} } +# Two-tier RBAC. Permission groups (akP-*) are the atomic units mapped to app +# roles and bound to applications for access. Role groups (akR-*) are what users +# are assigned to; each nests permission groups via `parents`, so a member of a +# role is an effective member of every permission it grants (Authentik membership +# propagates child -> parent). Split into two variables/resources so roles can +# reference permission group ids without the authentik_group self-reference error. +variable "permission_groups" { + type = map(object({ + name = string + # slug of the oauth2 application this permission grants *access* to; when set, + # a policy binding is created gating that app to this group (and its children). + application = optional(string, null) + attributes = optional(map(string), {}) + })) + default = {} +} + +variable "role_groups" { + type = map(object({ + name = string + # keys into var.permission_groups that this role nests (becomes its parents). + permissions = optional(list(string), []) + is_superuser = optional(bool, false) + attributes = optional(map(string), {}) + })) + default = {} +} + variable "providers_saml" { type = map(object({ name = string From 805ea48a36457b793deda5acfe8cd83ed880fd68 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sat, 18 Jul 2026 16:19:26 +1000 Subject: [PATCH 2/2] Derive group name from filename; use distinct ak_groups claim - Permission/role group name now comes from the config filename (the map key), dropping the redundant `name` field from each YAML and the object types. - The hierarchical mapping emits an `ak_groups` claim (scope `ak_groups`) instead of `groups`, so it never collides with the direct-groups the default profile mapping already emits under `groups` (Authentik overrides same-key claims in an unpredictable order). Apps request the `ak_groups` scope and read that claim. --- config/permissions/ak-p-argocd-admin.yaml | 4 ---- config/permissions/ak-p-argocd-user.yaml | 4 ---- config/permissions/ak-p-grafana-admin.yaml | 4 ---- config/permissions/ak-p-grafana-user.yaml | 4 ---- config/permissions/ak-p-rancher-admin.yaml | 4 ---- config/permissions/ak-p-rancher-user.yaml | 4 ---- config/permissions/akP-argocd-admin.yaml | 3 +++ config/permissions/akP-argocd-user.yaml | 3 +++ config/permissions/akP-grafana-admin.yaml | 3 +++ config/permissions/akP-grafana-user.yaml | 3 +++ config/permissions/akP-rancher-admin.yaml | 3 +++ config/permissions/akP-rancher-user.yaml | 3 +++ config/roles/ak-r-global-admin.yaml | 7 ------- config/roles/ak-r-standard-user.yaml | 6 ------ config/roles/akR-global-admin.yaml | 5 +++++ config/roles/akR-standard-user.yaml | 5 +++++ modules/authentik/main.tf | 23 +++++++++++----------- modules/authentik/variables.tf | 3 +-- 18 files changed, 41 insertions(+), 50 deletions(-) delete mode 100644 config/permissions/ak-p-argocd-admin.yaml delete mode 100644 config/permissions/ak-p-argocd-user.yaml delete mode 100644 config/permissions/ak-p-grafana-admin.yaml delete mode 100644 config/permissions/ak-p-grafana-user.yaml delete mode 100644 config/permissions/ak-p-rancher-admin.yaml delete mode 100644 config/permissions/ak-p-rancher-user.yaml create mode 100644 config/permissions/akP-argocd-admin.yaml create mode 100644 config/permissions/akP-argocd-user.yaml create mode 100644 config/permissions/akP-grafana-admin.yaml create mode 100644 config/permissions/akP-grafana-user.yaml create mode 100644 config/permissions/akP-rancher-admin.yaml create mode 100644 config/permissions/akP-rancher-user.yaml delete mode 100644 config/roles/ak-r-global-admin.yaml delete mode 100644 config/roles/ak-r-standard-user.yaml create mode 100644 config/roles/akR-global-admin.yaml create mode 100644 config/roles/akR-standard-user.yaml diff --git a/config/permissions/ak-p-argocd-admin.yaml b/config/permissions/ak-p-argocd-admin.yaml deleted file mode 100644 index 22f7078..0000000 --- a/config/permissions/ak-p-argocd-admin.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# Permission: admin access to argocd. Bound to the argocd application for -# access, and mapped to the argocd admin role via the groups claim. -name: akP-argocd-admin -application: argocd diff --git a/config/permissions/ak-p-argocd-user.yaml b/config/permissions/ak-p-argocd-user.yaml deleted file mode 100644 index b369880..0000000 --- a/config/permissions/ak-p-argocd-user.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# Permission: user access to argocd. Bound to the argocd application for -# access, and mapped to the argocd user role via the groups claim. -name: akP-argocd-user -application: argocd diff --git a/config/permissions/ak-p-grafana-admin.yaml b/config/permissions/ak-p-grafana-admin.yaml deleted file mode 100644 index 955e29a..0000000 --- a/config/permissions/ak-p-grafana-admin.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# Permission: admin access to grafana. Bound to the grafana application for -# access, and mapped to the grafana admin role via the groups claim. -name: akP-grafana-admin -application: grafana diff --git a/config/permissions/ak-p-grafana-user.yaml b/config/permissions/ak-p-grafana-user.yaml deleted file mode 100644 index 2092784..0000000 --- a/config/permissions/ak-p-grafana-user.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# Permission: user access to grafana. Bound to the grafana application for -# access, and mapped to the grafana user role via the groups claim. -name: akP-grafana-user -application: grafana diff --git a/config/permissions/ak-p-rancher-admin.yaml b/config/permissions/ak-p-rancher-admin.yaml deleted file mode 100644 index 1acc5e5..0000000 --- a/config/permissions/ak-p-rancher-admin.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# Permission: admin access to rancher. Bound to the rancher application for -# access, and mapped to the rancher admin role via the groups claim. -name: akP-rancher-admin -application: rancher diff --git a/config/permissions/ak-p-rancher-user.yaml b/config/permissions/ak-p-rancher-user.yaml deleted file mode 100644 index d596373..0000000 --- a/config/permissions/ak-p-rancher-user.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# Permission: user access to rancher. Bound to the rancher application for -# access, and mapped to the rancher user role via the groups claim. -name: akP-rancher-user -application: rancher diff --git a/config/permissions/akP-argocd-admin.yaml b/config/permissions/akP-argocd-admin.yaml new file mode 100644 index 0000000..50af97c --- /dev/null +++ b/config/permissions/akP-argocd-admin.yaml @@ -0,0 +1,3 @@ +# Permission group akP-argocd-admin (name = filename). Grants admin +# access to argocd: bound to the argocd application and mapped to its admin role. +application: argocd diff --git a/config/permissions/akP-argocd-user.yaml b/config/permissions/akP-argocd-user.yaml new file mode 100644 index 0000000..c0ad827 --- /dev/null +++ b/config/permissions/akP-argocd-user.yaml @@ -0,0 +1,3 @@ +# Permission group akP-argocd-user (name = filename). Grants user +# access to argocd: bound to the argocd application and mapped to its user role. +application: argocd diff --git a/config/permissions/akP-grafana-admin.yaml b/config/permissions/akP-grafana-admin.yaml new file mode 100644 index 0000000..ee8ff1f --- /dev/null +++ b/config/permissions/akP-grafana-admin.yaml @@ -0,0 +1,3 @@ +# Permission group akP-grafana-admin (name = filename). Grants admin +# access to grafana: bound to the grafana application and mapped to its admin role. +application: grafana diff --git a/config/permissions/akP-grafana-user.yaml b/config/permissions/akP-grafana-user.yaml new file mode 100644 index 0000000..718c224 --- /dev/null +++ b/config/permissions/akP-grafana-user.yaml @@ -0,0 +1,3 @@ +# Permission group akP-grafana-user (name = filename). Grants user +# access to grafana: bound to the grafana application and mapped to its user role. +application: grafana diff --git a/config/permissions/akP-rancher-admin.yaml b/config/permissions/akP-rancher-admin.yaml new file mode 100644 index 0000000..e312834 --- /dev/null +++ b/config/permissions/akP-rancher-admin.yaml @@ -0,0 +1,3 @@ +# Permission group akP-rancher-admin (name = filename). Grants admin +# access to rancher: bound to the rancher application and mapped to its admin role. +application: rancher diff --git a/config/permissions/akP-rancher-user.yaml b/config/permissions/akP-rancher-user.yaml new file mode 100644 index 0000000..e59c1d1 --- /dev/null +++ b/config/permissions/akP-rancher-user.yaml @@ -0,0 +1,3 @@ +# Permission group akP-rancher-user (name = filename). Grants user +# access to rancher: bound to the rancher application and mapped to its user role. +application: rancher diff --git a/config/roles/ak-r-global-admin.yaml b/config/roles/ak-r-global-admin.yaml deleted file mode 100644 index d9d0a7a..0000000 --- a/config/roles/ak-r-global-admin.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# Role: full admin across all onboarded apps. Assign users here for org-wide admin. -name: akR-global-admin -is_superuser: false -permissions: - - ak-p-grafana-admin - - ak-p-argocd-admin - - ak-p-rancher-admin diff --git a/config/roles/ak-r-standard-user.yaml b/config/roles/ak-r-standard-user.yaml deleted file mode 100644 index 8e21d35..0000000 --- a/config/roles/ak-r-standard-user.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Role: standard (non-admin) access across all onboarded apps. -name: akR-standard-user -permissions: - - ak-p-grafana-user - - ak-p-argocd-user - - ak-p-rancher-user diff --git a/config/roles/akR-global-admin.yaml b/config/roles/akR-global-admin.yaml new file mode 100644 index 0000000..cb24e50 --- /dev/null +++ b/config/roles/akR-global-admin.yaml @@ -0,0 +1,5 @@ +# Role akR-global-admin (name = filename): full admin across all onboarded apps. +permissions: + - akP-grafana-admin + - akP-argocd-admin + - akP-rancher-admin diff --git a/config/roles/akR-standard-user.yaml b/config/roles/akR-standard-user.yaml new file mode 100644 index 0000000..14b8836 --- /dev/null +++ b/config/roles/akR-standard-user.yaml @@ -0,0 +1,5 @@ +# Role akR-standard-user (name = filename): standard (non-admin) access everywhere. +permissions: + - akP-grafana-user + - akP-argocd-user + - akP-rancher-user diff --git a/modules/authentik/main.tf b/modules/authentik/main.tf index 69f6e8f..fa84126 100644 --- a/modules/authentik/main.tf +++ b/modules/authentik/main.tf @@ -12,7 +12,7 @@ resource "authentik_group" "this" { resource "authentik_group" "permission" { for_each = var.permission_groups - name = each.value.name + name = each.key attributes = jsonencode(each.value.attributes) } @@ -23,21 +23,22 @@ resource "authentik_group" "permission" { resource "authentik_group" "role" { for_each = var.role_groups - name = each.value.name + name = each.key is_superuser = each.value.is_superuser parents = [for p in each.value.permissions : authentik_group.permission[p].id] attributes = jsonencode(each.value.attributes) } -# Expand the OIDC `groups` claim to include inherited (ancestor) groups. The -# default profile mapping only emits direct groups (goauthentik/authentik#15579), -# so a member of a role group would not see the permission groups it nests. This -# walks each of the user's direct groups up through `.parents` and emits the full -# set of names, so role -> permission nesting drives in-app roles. Only evaluated -# when a provider requests the `groups` scope. +# Emit an `ak_groups` claim containing the user's groups AND all inherited +# (ancestor) groups, so role -> permission nesting reaches apps. The default +# profile mapping only emits *direct* groups under `groups` +# (goauthentik/authentik#15579); we use a distinct claim key so there is no +# collision with that (Authentik dict-overrides same-key claims in an +# unpredictable order). Apps request the `ak_groups` scope and read the +# `ak_groups` claim. Walks each direct group up through `.parents`. resource "authentik_property_mapping_provider_scope" "groups_hierarchical" { - name = "unkin: groups (hierarchical)" - scope_name = "groups" + name = "unkin: ak_groups (hierarchical)" + scope_name = "ak_groups" expression = <<-EOT groups = {} pending = list(user.ak_groups.all()) @@ -47,7 +48,7 @@ resource "authentik_property_mapping_provider_scope" "groups_hierarchical" { continue groups[grp.pk] = grp.name pending += list(grp.parents.all()) - return {"groups": sorted(groups.values())} + return {"ak_groups": sorted(groups.values())} EOT } diff --git a/modules/authentik/variables.tf b/modules/authentik/variables.tf index 86bc700..3569aec 100644 --- a/modules/authentik/variables.tf +++ b/modules/authentik/variables.tf @@ -16,9 +16,9 @@ variable "groups" { # role is an effective member of every permission it grants (Authentik membership # propagates child -> parent). Split into two variables/resources so roles can # reference permission group ids without the authentik_group self-reference error. +# The group name is the map key (the config filename); no `name` field needed. variable "permission_groups" { type = map(object({ - name = string # slug of the oauth2 application this permission grants *access* to; when set, # a policy binding is created gating that app to this group (and its children). application = optional(string, null) @@ -29,7 +29,6 @@ variable "permission_groups" { variable "role_groups" { type = map(object({ - name = string # keys into var.permission_groups that this role nests (becomes its parents). permissions = optional(list(string), []) is_superuser = optional(bool, false)