Fix RBAC groups handling and refactor Keycloak role

- Fixed incorrect handling of RBAC group configuration (moved from OIDC claims into dedicated RBAC variable set).
- Unified RBAC group usage across applications (LAM, pgAdmin, phpLDAPadmin, phpMyAdmin, YOURLS).
- Replaced old 'KEYCLOAK_OIDC_RBAC_SCOPE_NAME' with dedicated 'KEYCLOAK_RBAC_GROUP_*' variables.
- Updated OAuth2 Proxy configuration to use 'RBAC.GROUP.CLAIM'.
- Refactored Keycloak role task structure:
  * Renamed and reorganized task files for clarity ('_update.yml', '02_cleanup.yml', etc.).
  * Introduced meta and dependency handling separation.
- Cleaned up Keycloak config defaults and recaptcha placeholders.
This commit is contained in:
Kevin Veen-Birkenbach 2025-08-17 23:27:01 +02:00
commit d1cd87c843
No known key found for this signature in database
GPG Key ID: 44D8F11FD62F878E
24 changed files with 66 additions and 58 deletions

View File

@ -91,3 +91,10 @@ _applications_nextcloud_oidc_flavor: >-
# Systemctl # Systemctl
SYS_TIMER_SUFFIX: ".{{ SOFTWARE_NAME | lower }}.timer" SYS_TIMER_SUFFIX: ".{{ SOFTWARE_NAME | lower }}.timer"
SYS_SERVICE_SUFFIX: ".{{ SOFTWARE_NAME | lower }}.service" SYS_SERVICE_SUFFIX: ".{{ SOFTWARE_NAME | lower }}.service"
# Role-based access control
# @See https://en.wikipedia.org/wiki/Role-based_access_control
RBAC:
GROUP:
NAME: "/roles" # Name of the group which holds the RBAC roles
CLAIM: "groups" # Name of the claim containing the RBAC groups

View File

@ -40,5 +40,3 @@ defaults_oidc:
GIVEN_NAME: "givenName" GIVEN_NAME: "givenName"
FAMILY_NAME: "surname" FAMILY_NAME: "surname"
EMAIL: "email" EMAIL: "email"
CLAIMS:
GROUPS: "groups"

View File

@ -32,7 +32,6 @@ server:
scopes: scopes:
nextcloud: nextcloud nextcloud: nextcloud
rbac_groups: "/rbac"
docker: docker:
services: services:
keycloak: keycloak:
@ -44,5 +43,5 @@ docker:
credentials: credentials:
recaptcha: recaptcha:
website_key: "YOUR_RECAPTCHA_WEBSITE_KEY" # Required if you enabled recaptcha: website_key: "" # Required if you enabled recaptcha:
secret_key: "YOUR_RECAPTCHA_SECRET_KEY" # Required if you enabled recaptcha: secret_key: "" # Required if you enabled recaptcha:

View File

@ -0,0 +1,8 @@
- include_tasks: "{{ playbook_dir }}/tasks/utils/load_handlers.yml"
vars:
handler_role_name: "docker-compose"
- ansible.builtin.include_vars:
file: "{{ item }}"
loop:
- "{{ playbook_dir }}/roles/docker-compose/vars/docker-compose.yml"
- "{{ playbook_dir }}/roles/cmp-rdbms/vars/database.yml"

View File

@ -1,25 +0,0 @@
- name: Load the '{{ application_id }}' role dependencies
block:
- name: "load required 'web-svc-logout' for {{ application_id }}"
include_role:
name: web-svc-logout
when: run_once_web_svc_logout is not defined
- name: "load docker, db and proxy for {{ application_id }}"
include_role:
name: cmp-db-docker-proxy
vars:
docker_compose_flush_handlers: true
when: KEYCLOAK_LOAD_DEPENDENCIES | bool
- name: "Load database & docker-compose variables if role dependencies aren't loaded"
block:
- include_tasks: "{{ playbook_dir }}/tasks/utils/load_handlers.yml"
vars:
handler_role_name: "docker-compose"
- ansible.builtin.include_vars:
file: "{{ item }}"
loop:
- "{{ playbook_dir }}/roles/docker-compose/vars/docker-compose.yml"
- "{{ playbook_dir }}/roles/cmp-rdbms/vars/database.yml"
when: not KEYCLOAK_LOAD_DEPENDENCIES | bool

View File

@ -0,0 +1,12 @@
- block:
- name: "load required 'web-svc-logout' for {{ application_id }}"
include_role:
name: web-svc-logout
public: false
when: run_once_web_svc_logout is not defined
- name: "load docker, db and proxy for {{ application_id }}"
include_role:
name: cmp-db-docker-proxy
vars:
docker_compose_flush_handlers: true

View File

@ -5,7 +5,7 @@
{{ {{
( (
KEYCLOAK_DICTIONARY_REALM.clientScopes KEYCLOAK_DICTIONARY_REALM.clientScopes
| selectattr('name','equalto', KEYCLOAK_OIDC_RBAC_SCOPE_NAME) | selectattr('name','equalto', KEYCLOAK_RBAC_GROUP_CLAIME)
| list | first | list | first
) | to_json ) | to_json
}} }}
@ -27,7 +27,7 @@
scope_id_rbac: >- scope_id_rbac: >-
{{ ( {{ (
all_scopes.stdout | from_json all_scopes.stdout | from_json
| selectattr('name','equalto', KEYCLOAK_OIDC_RBAC_SCOPE_NAME) | selectattr('name','equalto', KEYCLOAK_RBAC_GROUP_CLAIME)
| list | first | default({}) | list | first | default({})
).id | default('') }} ).id | default('') }}

View File

@ -1,5 +1,5 @@
- name: "Update REALM settings (merge LDAP component .config)" - name: "Update REALM settings (merge LDAP component .config)"
include_tasks: 03_update.yml include_tasks: _update.yml
vars: vars:
kc_object_kind: "component" kc_object_kind: "component"
kc_lookup_value: "{{ KEYCLOAK_LDAP_CMP_NAME }}" kc_lookup_value: "{{ KEYCLOAK_LDAP_CMP_NAME }}"
@ -141,4 +141,4 @@
}, recursive=True) }, recursive=True)
}} }}
kc_merge_path: "config" kc_merge_path: "config"
include_tasks: 03_update.yml include_tasks: _update.yml

View File

@ -1,12 +1,18 @@
--- ---
- name: "Load meta"
include_tasks: 01_meta.yml
when: not KEYCLOAK_LOAD_DEPENDENCIES | bool
- name: "Load cleanup routine for '{{ application_id }}'" - name: "Load cleanup routine for '{{ application_id }}'"
include_tasks: 01_cleanup.yml include_tasks: 02_cleanup.yml
- name: "Load init routine for '{{ application_id }}'" - name: "Load init routine for '{{ application_id }}'"
include_tasks: 02_initialize.yml include_tasks: 03_init.yml
- name: "Load the depdendencies required by '{{ application_id }}'" - name: "Load the depdendencies required by '{{ application_id }}'"
include_tasks: 03_load_dependencies.yml include_tasks: 04_dependencies.yml
when: KEYCLOAK_LOAD_DEPENDENCIES | bool
- name: "Wait until '{{ KEYCLOAK_CONTAINER }}' container is healthy" - name: "Wait until '{{ KEYCLOAK_CONTAINER }}' container is healthy"
community.docker.docker_container_info: community.docker.docker_container_info:
@ -50,9 +56,9 @@
| list | first | default({}) ).attributes | default({}) ) | list | first | default({}) ).attributes | default({}) )
| combine({'frontchannel.logout.url': KEYCLOAK_FRONTCHANNEL_LOGOUT_URL}, recursive=True) | combine({'frontchannel.logout.url': KEYCLOAK_FRONTCHANNEL_LOGOUT_URL}, recursive=True)
}} }}
include_tasks: 03_update.yml include_tasks: _update.yml
- include_tasks: 04_rbac_client_scope.yml - include_tasks: 05_rbac_client_scope.yml
- include_tasks: 05_ldap.yml - include_tasks: 06_ldap.yml
when: KEYCLOAK_LDAP_ENABLED | bool when: KEYCLOAK_LDAP_ENABLED | bool

View File

@ -54,7 +54,7 @@
"organization", "organization",
"offline_access", "offline_access",
"microprofile-jwt", "microprofile-jwt",
"{{ KEYCLOAK_OIDC_RBAC_SCOPE_NAME }}", "{{ KEYCLOAK_RBAC_GROUP_CLAIME }}",
"{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}" "{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}"
] ]
} }

View File

@ -156,7 +156,7 @@
"group.object.classes": [ "groupOfNames" ], "group.object.classes": [ "groupOfNames" ],
"memberof.ldap.attribute": [ "memberOf" ], "memberof.ldap.attribute": [ "memberOf" ],
"drop.non.existing.groups.during.sync": [ "false" ], "drop.non.existing.groups.during.sync": [ "false" ],
"groups.path": [ "{{ applications | get_app_conf(application_id, 'rbac_groups', True) }}" ] "groups.path": [ "{{ KEYCLOAK_RBAC_GROUP_NAME }}" ]
} }
}, },
{ {

View File

@ -1430,7 +1430,7 @@
"phone", "phone",
"microprofile-jwt", "microprofile-jwt",
"organization", "organization",
"{{ KEYCLOAK_OIDC_RBAC_SCOPE_NAME }}", "{{ KEYCLOAK_RBAC_GROUP_CLAIME }}",
"{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}" "{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}"
], ],
"browserSecurityHeaders": { "browserSecurityHeaders": {

View File

@ -1,5 +1,5 @@
{ {
"name": "{{ KEYCLOAK_OIDC_RBAC_SCOPE_NAME }}", "name": "{{ KEYCLOAK_RBAC_GROUP_CLAIME }}",
"description": "RBAC Groups", "description": "RBAC Groups",
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": { "attributes": {
@ -22,7 +22,7 @@
"id.token.claim": "true", "id.token.claim": "true",
"lightweight.claim": "false", "lightweight.claim": "false",
"access.token.claim": "true", "access.token.claim": "true",
"claim.name": "{{ OIDC.CLAIMS.GROUPS }}" "claim.name": "{{ KEYCLOAK_RBAC_GROUP_CLAIME }}"
} }
} }
] ]

View File

@ -11,9 +11,12 @@ KEYCLOAK_REALM_URL: "{{ WEB_PROTOCOL }}://{{ KEYCLOAK_REALM }}"
KEYCLOAK_DEBUG_ENABLED: "{{ MODE_DEBUG }}" KEYCLOAK_DEBUG_ENABLED: "{{ MODE_DEBUG }}"
KEYCLOAK_CLIENT_ID: "{{ OIDC.CLIENT.ID }}" KEYCLOAK_CLIENT_ID: "{{ OIDC.CLIENT.ID }}"
KEYCLOAK_SERVER_INTERNAL_URL: "http://127.0.0.1:8080" KEYCLOAK_SERVER_INTERNAL_URL: "http://127.0.0.1:8080"
KEYCLOAK_OIDC_RBAC_SCOPE_NAME: "{{ OIDC.CLAIMS.GROUPS }}"
KEYCLOAK_LOAD_DEPENDENCIES: "{{ applications | get_app_conf(application_id, 'load_dependencies') }}" KEYCLOAK_LOAD_DEPENDENCIES: "{{ applications | get_app_conf(application_id, 'load_dependencies') }}"
# RBAC
KEYCLOAK_RBAC_GROUP_CLAIME: "{{ RBAC.GROUP.CLAIM }}"
KEYCLOAK_RBAC_GROUP_NAME: "{{ RBAC.GROUP.NAME }}"
## Health ## Health
KEYCLOAK_HEALTH_ENABLED: true KEYCLOAK_HEALTH_ENABLED: true

View File

@ -7,7 +7,7 @@ oauth2_proxy:
application: application application: application
port: 80 port: 80
allowed_groups: allowed_groups:
- "/roles/web-app-lam-administrator" - "{{ [RBAC.GROUP.NAME, 'web-app-lam-administrator'] | path_join }}"
features: features:
matomo: true matomo: true
css: true css: true

View File

@ -15,8 +15,8 @@ provider_display_name = "{{ OIDC.BUTTON_TEXT }}"
{% if applications | get_app_conf(oauth2_proxy_application_id, 'oauth2_proxy.allowed_groups', False) %} {% if applications | get_app_conf(oauth2_proxy_application_id, 'oauth2_proxy.allowed_groups', False) %}
{# role based restrictions #} {# role based restrictions #}
scope = "openid email profile {{ OIDC.CLAIMS.GROUPS }}" scope = "openid email profile {{ RBAC.GROUP.CLAIM }}"
oidc_groups_claim = "{{ OIDC.CLAIMS.GROUPS }}" oidc_groups_claim = "{{ RBAC.GROUP.CLAIM }}"
allowed_groups = {{ applications | get_app_conf(oauth2_proxy_application_id, 'oauth2_proxy.allowed_groups', True) | tojson }} allowed_groups = {{ applications | get_app_conf(oauth2_proxy_application_id, 'oauth2_proxy.allowed_groups', True) | tojson }}
email_domains = ["*"] email_domains = ["*"]
{% else %} {% else %}

View File

@ -5,7 +5,7 @@ oauth2_proxy:
application: "application" application: "application"
port: "80" port: "80"
allowed_groups: allowed_groups:
- "/roles/web-app-pgadmin-administrator" - "{{ [RBAC.GROUP.NAME, 'web-app-pgadmin-administrator'] | path_join }}"
features: features:
matomo: true matomo: true
css: true css: true

View File

@ -3,7 +3,7 @@ oauth2_proxy:
application: application # Needs to be the same as webinterface application: application # Needs to be the same as webinterface
port: 8080 # application port port: 8080 # application port
allowed_groups: allowed_groups:
- "/roles/web-app-phpldapadmin-administrator" - "{{ [RBAC.GROUP.NAME, 'web-app-phpldapadmin-administrator'] | path_join }}"
features: features:
matomo: true matomo: true
css: true css: true

View File

@ -3,7 +3,7 @@ oauth2_proxy:
port: "80" port: "80"
application: "application" application: "application"
allowed_groups: allowed_groups:
- "/roles/web-app-phpmyadmin-administrator" - "{{ [RBAC.GROUP.NAME, 'web-app-phpmyadmin-administrator'] | path_join }}"
features: features:
matomo: true matomo: true
css: false css: false

View File

@ -2,7 +2,7 @@ oauth2_proxy:
application: "application" application: "application"
port: "80" port: "80"
allowed_groups: allowed_groups:
- "/roles/web-app-yourls-administrator" - "{{ [RBAC.GROUP.NAME, 'web-app-yourls-administrator'] | path_join }}"
acl: acl:
blacklist: blacklist:
- "<< defaults_applications[web-app-yourls].server.locations.admin >>" # Protects the admin area - "<< defaults_applications[web-app-yourls].server.locations.admin >>" # Protects the admin area