Add two-tier RBAC: permission/role groups, access policies, hierarchical group claim #7
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -7,6 +7,51 @@ 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.key
|
||||
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.key
|
||||
is_superuser = each.value.is_superuser
|
||||
parents = [for p in each.value.permissions : authentik_group.permission[p].id]
|
||||
attributes = jsonencode(each.value.attributes)
|
||||
}
|
||||
|
||||
# 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: ak_groups (hierarchical)"
|
||||
scope_name = "ak_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 {"ak_groups": sorted(groups.values())}
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "authentik_provider_saml" "this" {
|
||||
for_each = var.providers_saml
|
||||
|
||||
@@ -46,13 +91,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 +153,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
|
||||
}
|
||||
|
||||
@@ -10,6 +10,33 @@ 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.
|
||||
# The group name is the map key (the config filename); no `name` field needed.
|
||||
variable "permission_groups" {
|
||||
type = map(object({
|
||||
# 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({
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user