Add LiteLLM RBAC: akP-litellm groups + role claim mapping #10
@@ -0,0 +1,3 @@
|
|||||||
|
# Permission group akP-litellm-admin (name = filename). Grants admin
|
||||||
|
# access to litellm: bound to the litellm application and mapped to its role.
|
||||||
|
application: litellm
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Permission group akP-litellm-user (name = filename). Grants user
|
||||||
|
# access to litellm: bound to the litellm application and mapped to its role.
|
||||||
|
application: litellm
|
||||||
@@ -16,3 +16,14 @@ scope_mappings:
|
|||||||
redirect_uris:
|
redirect_uris:
|
||||||
- matching_mode: strict
|
- matching_mode: strict
|
||||||
url: https://litellm.k8s.syd1.au.unkin.net/sso/callback
|
url: https://litellm.k8s.syd1.au.unkin.net/sso/callback
|
||||||
|
# Emit a `litellm_role` claim from group membership (rules in privilege order).
|
||||||
|
# LiteLLM requests the `litellm_role` scope and reads it via
|
||||||
|
# GENERIC_USER_ROLE_ATTRIBUTE. akP-litellm-admin is inherited by akR-global-admin.
|
||||||
|
role_mappings:
|
||||||
|
claim: litellm_role
|
||||||
|
default: internal_user_view_only
|
||||||
|
rules:
|
||||||
|
- group: akP-litellm-admin
|
||||||
|
role: proxy_admin
|
||||||
|
- group: akP-litellm-user
|
||||||
|
role: internal_user
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ permissions:
|
|||||||
- akP-grafana-admin
|
- akP-grafana-admin
|
||||||
- akP-argocd-admin
|
- akP-argocd-admin
|
||||||
- akP-rancher-admin
|
- akP-rancher-admin
|
||||||
|
- akP-litellm-admin
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ permissions:
|
|||||||
- akP-grafana-user
|
- akP-grafana-user
|
||||||
- akP-argocd-user
|
- akP-argocd-user
|
||||||
- akP-rancher-user
|
- akP-rancher-user
|
||||||
|
- akP-litellm-user
|
||||||
|
|||||||
@@ -65,6 +65,43 @@ resource "authentik_provider_saml" "this" {
|
|||||||
signing_kp = each.value.signing_kp
|
signing_kp = each.value.signing_kp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Build a Python expression per app that emits a role claim from the user's
|
||||||
|
# effective (hierarchical) group membership. Assembled from the config rules so
|
||||||
|
# the generated code has predictable indentation (no template-directive quirks).
|
||||||
|
locals {
|
||||||
|
role_mapping_expr = {
|
||||||
|
for k, v in var.providers_oauth2 : k => join("\n", concat(
|
||||||
|
[
|
||||||
|
"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())",
|
||||||
|
"names = set(groups.values())",
|
||||||
|
],
|
||||||
|
flatten([
|
||||||
|
for rule in coalesce(try(v.role_mappings.rules, null), []) : [
|
||||||
|
"if ${jsonencode(rule.group)} in names:",
|
||||||
|
" return {${jsonencode(try(v.role_mappings.claim, ""))}: ${jsonencode(rule.role)}}",
|
||||||
|
]
|
||||||
|
]),
|
||||||
|
["return {${jsonencode(try(v.role_mappings.claim, ""))}: ${jsonencode(try(v.role_mappings.default, ""))}}"],
|
||||||
|
))
|
||||||
|
if v.role_mappings != null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "authentik_property_mapping_provider_scope" "role" {
|
||||||
|
for_each = { for k, v in var.providers_oauth2 : k => v if v.role_mappings != null }
|
||||||
|
|
||||||
|
name = "unkin: ${each.key} role"
|
||||||
|
scope_name = each.value.role_mappings.claim
|
||||||
|
expression = local.role_mapping_expr[each.key]
|
||||||
|
}
|
||||||
|
|
||||||
# Resolve oauth2 flows by slug and scope mappings by managed identifier, and
|
# Resolve oauth2 flows by slug and scope mappings by managed identifier, and
|
||||||
# read client secrets from Vault so nothing sensitive is committed.
|
# read client secrets from Vault so nothing sensitive is committed.
|
||||||
data "authentik_flow" "oauth2_authorization" {
|
data "authentik_flow" "oauth2_authorization" {
|
||||||
@@ -100,6 +137,7 @@ resource "authentik_provider_oauth2" "this" {
|
|||||||
property_mappings = concat(
|
property_mappings = concat(
|
||||||
try(data.authentik_property_mapping_provider_scope.oauth2[each.key].ids, []),
|
try(data.authentik_property_mapping_provider_scope.oauth2[each.key].ids, []),
|
||||||
[authentik_property_mapping_provider_scope.groups_hierarchical.id],
|
[authentik_property_mapping_provider_scope.groups_hierarchical.id],
|
||||||
|
try([authentik_property_mapping_provider_scope.role[each.key].id], []),
|
||||||
)
|
)
|
||||||
signing_key = each.value.signing_key
|
signing_key = each.value.signing_key
|
||||||
access_token_validity = each.value.access_token_validity
|
access_token_validity = each.value.access_token_validity
|
||||||
|
|||||||
@@ -76,6 +76,18 @@ variable "providers_oauth2" {
|
|||||||
})), [])
|
})), [])
|
||||||
signing_key = optional(string, null)
|
signing_key = optional(string, null)
|
||||||
access_token_validity = optional(string, "minutes=10")
|
access_token_validity = optional(string, "minutes=10")
|
||||||
|
# Optional app-role claim computed from (hierarchical) group membership: emit
|
||||||
|
# `claim` = the first matching rule's role, else `default`. The app requests
|
||||||
|
# `claim` as a scope and reads it (e.g. LiteLLM GENERIC_USER_ROLE_ATTRIBUTE).
|
||||||
|
# rules are evaluated in order, so list highest privilege first.
|
||||||
|
role_mappings = optional(object({
|
||||||
|
claim = string
|
||||||
|
default = string
|
||||||
|
rules = list(object({
|
||||||
|
group = string # permission group name (akP-*)
|
||||||
|
role = string # app role value
|
||||||
|
}))
|
||||||
|
}), null)
|
||||||
}))
|
}))
|
||||||
default = {}
|
default = {}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user