Manage human role membership from config/users/
Add a users/ config kind mapping a human to the akR-* roles they hold. Look accounts up with data.authentik_user; never declare them. Set authentik_group.role users only for roles a user file names, leaving every other role's membership untouched. No assignments yet.
This commit is contained in:
@@ -4,7 +4,8 @@ Terraform configuration for managing the Authentik identity provider at identity
|
||||
|
||||
## Managed Resources
|
||||
|
||||
- **Groups** — roles and group hierarchy (users are invited manually)
|
||||
- **Groups** — roles and group hierarchy (accounts themselves are created elsewhere)
|
||||
- **User role membership** — which `akR-*` roles a human holds (see `config/users/`)
|
||||
- **SAML providers** — SAML application integrations
|
||||
- **OAuth2/OIDC providers** — OAuth2 and OpenID Connect integrations
|
||||
- **LDAP providers** — LDAP provider and outpost configuration
|
||||
@@ -21,7 +22,8 @@ config/
|
||||
├── providers_saml/ # SAML provider definitions
|
||||
├── providers_oauth2/ # OAuth2/OIDC provider definitions
|
||||
├── providers_ldap/ # LDAP provider definitions
|
||||
└── service_accounts/ # Automation service accounts + API tokens
|
||||
├── service_accounts/ # Automation service accounts + API tokens
|
||||
└── users/ # Human role membership (authoritative per named role)
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -42,5 +42,10 @@ locals {
|
||||
trimsuffix(basename(file_path), ".yaml") => content
|
||||
if startswith(file_path, "service_accounts/")
|
||||
}
|
||||
users = {
|
||||
for file_path, content in local.all_configs :
|
||||
trimsuffix(basename(file_path), ".yaml") => content
|
||||
if startswith(file_path, "users/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# users
|
||||
|
||||
One file per human, `<username>.yaml`, listing the `akR-*` roles they hold:
|
||||
|
||||
```yaml
|
||||
# Human user jane (username = filename). The account itself is not managed here
|
||||
# (humans come from LDAP sync / invite); only its role membership is.
|
||||
roles:
|
||||
- akR-media-adult
|
||||
```
|
||||
|
||||
The account is looked up by username and must already exist — nothing here
|
||||
creates users. A role that has no `config/roles/<name>.yaml` fails the plan.
|
||||
|
||||
**Naming a role here makes Terraform authoritative over that role's entire
|
||||
member list**: members added by hand in the Authentik UI for that role are
|
||||
removed on the next apply. Roles no user file names are left untouched.
|
||||
@@ -24,4 +24,5 @@ inputs = {
|
||||
providers_oauth2 = local.config.providers_oauth2
|
||||
providers_ldap = local.config.providers_ldap
|
||||
service_accounts = local.config.service_accounts
|
||||
users = local.config.users
|
||||
}
|
||||
|
||||
@@ -16,6 +16,31 @@ resource "authentik_group" "permission" {
|
||||
attributes = jsonencode(each.value.attributes)
|
||||
}
|
||||
|
||||
# Humans are created outside this module (LDAP sync / invite), so they are
|
||||
# looked up rather than declared: resolve the username to the numeric pk that
|
||||
# group membership is keyed on. A missing account fails the plan; nothing here
|
||||
# can create a user.
|
||||
data "authentik_user" "human" {
|
||||
for_each = var.users
|
||||
username = each.key
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = length(setsubtract(each.value.roles, keys(var.role_groups))) == 0
|
||||
error_message = "config/users/${each.key}.yaml names a role with no config/roles/<name>.yaml."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
# Invert user -> roles into role -> member pks. Only roles some user file
|
||||
# names appear here; every other role falls through to a null `users` below.
|
||||
role_members = {
|
||||
for role in distinct(flatten([for u, v in var.users : v.roles])) :
|
||||
role => [for u, v in var.users : data.authentik_user.human[u].pk if contains(v.roles, role)]
|
||||
}
|
||||
}
|
||||
|
||||
# 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
|
||||
@@ -27,6 +52,10 @@ resource "authentik_group" "role" {
|
||||
is_superuser = each.value.is_superuser
|
||||
parents = [for p in each.value.permissions : authentik_group.permission[p].id]
|
||||
attributes = jsonencode(each.value.attributes)
|
||||
# Authoritative: a role claimed by config/users/ has exactly these members, so
|
||||
# one dropped from a user file is removed. null (every unclaimed role) leaves
|
||||
# the attribute computed, i.e. membership stays whatever Authentik holds.
|
||||
users = lookup(local.role_members, each.key, null)
|
||||
}
|
||||
|
||||
# Emit an `ak_groups` claim containing the user's groups AND all inherited
|
||||
|
||||
@@ -156,3 +156,18 @@ variable "service_accounts" {
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
# Human role membership. The username is the map key (the config filename). The
|
||||
# account itself is never managed here — humans are created by LDAP sync/invite
|
||||
# and only looked up — so this grants and revokes roles, it does not make users.
|
||||
#
|
||||
# OWNERSHIP: naming a role in any user file makes Terraform authoritative over
|
||||
# that role's entire member list, so members added by hand in the UI for that
|
||||
# role are removed on the next apply. Roles no user file names are left alone.
|
||||
variable "users" {
|
||||
type = map(object({
|
||||
# keys into var.role_groups (akR-*) this user is a member of.
|
||||
roles = optional(list(string), [])
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user