1dab2ecc6f
Introduce a user -> role -> [permissions] model for app access and roles, managed declaratively. - Permission groups (akP-<app>-<access>) under config/permissions/: atomic units, each names the application it grants access to. - Role groups (akR-<role>) 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`.
167 lines
6.0 KiB
Terraform
167 lines
6.0 KiB
Terraform
resource "authentik_group" "this" {
|
|
for_each = var.groups
|
|
|
|
name = each.value.name
|
|
is_superuser = each.value.is_superuser
|
|
parents = each.value.parents
|
|
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
|
|
|
|
name = each.value.name
|
|
authorization_flow = each.value.authorization_flow
|
|
invalidation_flow = each.value.invalidation_flow
|
|
acs_url = each.value.acs_url
|
|
sp_binding = each.value.sp_binding
|
|
audience = each.value.audience
|
|
name_id_mapping = each.value.name_id_mapping
|
|
signing_kp = each.value.signing_kp
|
|
}
|
|
|
|
# Resolve oauth2 flows by slug and scope mappings by managed identifier, and
|
|
# read client secrets from Vault so nothing sensitive is committed.
|
|
data "authentik_flow" "oauth2_authorization" {
|
|
for_each = var.providers_oauth2
|
|
slug = each.value.authorization_flow
|
|
}
|
|
|
|
data "authentik_flow" "oauth2_invalidation" {
|
|
for_each = var.providers_oauth2
|
|
slug = each.value.invalidation_flow
|
|
}
|
|
|
|
data "authentik_property_mapping_provider_scope" "oauth2" {
|
|
for_each = { for k, v in var.providers_oauth2 : k => v if length(v.scope_mappings) > 0 }
|
|
managed_list = each.value.scope_mappings
|
|
}
|
|
|
|
data "vault_kv_secret_v2" "oauth2" {
|
|
for_each = { for k, v in var.providers_oauth2 : k => v if v.client_secret_vault != null }
|
|
mount = each.value.client_secret_vault.mount
|
|
name = each.value.client_secret_vault.path
|
|
}
|
|
|
|
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 = 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
|
|
}
|
|
|
|
resource "authentik_provider_ldap" "this" {
|
|
for_each = var.providers_ldap
|
|
|
|
name = each.value.name
|
|
bind_flow = each.value.bind_flow
|
|
unbind_flow = each.value.unbind_flow
|
|
base_dn = each.value.base_dn
|
|
certificate = each.value.certificate
|
|
tls_server_name = each.value.tls_server_name
|
|
uid_start_number = each.value.uid_start_number
|
|
gid_start_number = each.value.gid_start_number
|
|
search_mode = each.value.search_mode
|
|
bind_mode = each.value.bind_mode
|
|
mfa_support = each.value.mfa_support
|
|
}
|
|
|
|
resource "authentik_application" "saml" {
|
|
for_each = var.providers_saml
|
|
|
|
name = each.value.name
|
|
slug = each.key
|
|
protocol_provider = authentik_provider_saml.this[each.key].id
|
|
}
|
|
|
|
resource "authentik_application" "oauth2" {
|
|
for_each = var.providers_oauth2
|
|
|
|
name = each.value.name
|
|
slug = each.key
|
|
protocol_provider = authentik_provider_oauth2.this[each.key].id
|
|
}
|
|
|
|
resource "authentik_application" "ldap" {
|
|
for_each = var.providers_ldap
|
|
|
|
name = each.value.name
|
|
slug = each.key
|
|
protocol_provider = authentik_provider_ldap.this[each.key].id
|
|
}
|
|
|
|
resource "authentik_outpost" "ldap" {
|
|
for_each = var.providers_ldap
|
|
|
|
name = "${each.key}-outpost"
|
|
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
|
|
}
|