mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-09-08 03:07:14 +02:00
Compare commits
8 Commits
7d0502ebc5
...
a5941763ff
Author | SHA1 | Date | |
---|---|---|---|
a5941763ff | |||
3d7bbabd7b | |||
e4b8c97e03 | |||
29df95ed82 | |||
6443771d93 | |||
d1cd87c843 | |||
5f0762e4f6 | |||
5642793f4a |
@@ -90,4 +90,11 @@ _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
|
@@ -39,6 +39,4 @@ defaults_oidc:
|
|||||||
USERNAME: "preferred_username"
|
USERNAME: "preferred_username"
|
||||||
GIVEN_NAME: "givenName"
|
GIVEN_NAME: "givenName"
|
||||||
FAMILY_NAME: "surname"
|
FAMILY_NAME: "surname"
|
||||||
EMAIL: "email"
|
EMAIL: "email"
|
||||||
CLAIMS:
|
|
||||||
GROUPS: "groups"
|
|
@@ -8,7 +8,7 @@ openldap_bind_dn: "{{ ldap.dn.administrator.configuration }}"
|
|||||||
openldap_bind_pw: "{{ applications | get_app_conf(application_id, 'credentials.administrator_password', True) }}"
|
openldap_bind_pw: "{{ applications | get_app_conf(application_id, 'credentials.administrator_password', True) }}"
|
||||||
|
|
||||||
# LDIF Variables
|
# LDIF Variables
|
||||||
openldap_ldif_host_path: "{{docker_compose.directories.volumes}}ldif/"
|
openldap_ldif_host_path: "{{ docker_compose.directories.volumes }}ldif/"
|
||||||
openldap_ldif_docker_path: "/tmp/ldif/"
|
openldap_ldif_docker_path: "/tmp/ldif/"
|
||||||
openldap_ldif_types:
|
openldap_ldif_types:
|
||||||
- configuration
|
- configuration
|
||||||
|
@@ -43,6 +43,8 @@
|
|||||||
- database_username is defined
|
- database_username is defined
|
||||||
- database_password is defined
|
- database_password is defined
|
||||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||||
|
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
|
||||||
|
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
|
||||||
|
|
||||||
- name: Set file permissions for databases.csv to be readable, writable, and executable by root only
|
- name: Set file permissions for databases.csv to be readable, writable, and executable by root only
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
@@ -57,6 +59,8 @@
|
|||||||
database_password is defined) and
|
database_password is defined) and
|
||||||
run_once_sys_bkp_docker_2_loc_file_permission is not defined
|
run_once_sys_bkp_docker_2_loc_file_permission is not defined
|
||||||
register: file_permission_result
|
register: file_permission_result
|
||||||
|
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
|
||||||
|
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
|
||||||
|
|
||||||
- name: run the backup_docker_to_local_file_permission tasks once
|
- name: run the backup_docker_to_local_file_permission tasks once
|
||||||
set_fact:
|
set_fact:
|
||||||
|
@@ -13,7 +13,7 @@
|
|||||||
include_role:
|
include_role:
|
||||||
name: cmp-docker-proxy
|
name: cmp-docker-proxy
|
||||||
vars:
|
vars:
|
||||||
docker_compose_flush_handlers: false
|
docker_compose_flush_handlers: false
|
||||||
- name: "include 04_seed-database-to-backup.yml"
|
- name: "include 04_seed-database-to-backup.yml"
|
||||||
include_tasks: "{{ playbook_dir }}/roles/sys-bkp-docker-2-loc/tasks/04_seed-database-to-backup.yml"
|
include_tasks: "{{ playbook_dir }}/roles/sys-bkp-docker-2-loc/tasks/04_seed-database-to-backup.yml"
|
||||||
|
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
load_dependencies: True # When set to false the dependencies aren't loaded. Helpful for developing
|
||||||
actions:
|
actions:
|
||||||
import_realm: True # Import REALM
|
import_realm: True # Import REALM
|
||||||
features:
|
features:
|
||||||
@@ -29,10 +30,8 @@ server:
|
|||||||
canonical:
|
canonical:
|
||||||
- "auth.{{ PRIMARY_DOMAIN }}"
|
- "auth.{{ PRIMARY_DOMAIN }}"
|
||||||
scopes:
|
scopes:
|
||||||
rbac_roles: rbac_roles
|
|
||||||
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:
|
47
roles/web-app-keycloak/filter_plugins/ldap_filters.py
Normal file
47
roles/web-app-keycloak/filter_plugins/ldap_filters.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
class FilterModule(object):
|
||||||
|
"""Custom Jinja2 filters for LDAP related rendering."""
|
||||||
|
|
||||||
|
def filters(self):
|
||||||
|
return {
|
||||||
|
"ldap_groups_filter": self.ldap_groups_filter,
|
||||||
|
}
|
||||||
|
|
||||||
|
def ldap_groups_filter(self, flavors, default="groupOfNames") -> str:
|
||||||
|
"""
|
||||||
|
Build an LDAP objectClass filter for groups based on available flavors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
flavors: list/tuple/set of enabled flavors (e.g. ["groupOfNames","organizationalUnit"])
|
||||||
|
default: fallback objectClass if nothing matches
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A *single-line* LDAP filter string suitable for JSON, e.g.:
|
||||||
|
(|(objectClass=groupOfNames)(objectClass=organizationalUnit))
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- If both groupOfNames and organizationalUnit are present -> OR them.
|
||||||
|
- If one of them is present -> use that one.
|
||||||
|
- Otherwise -> use `default`.
|
||||||
|
"""
|
||||||
|
if flavors is None:
|
||||||
|
flavors = []
|
||||||
|
if isinstance(flavors, str):
|
||||||
|
# be forgiving if someone passes a comma-separated string
|
||||||
|
flavors = [f.strip() for f in flavors.split(",") if f.strip()]
|
||||||
|
if not isinstance(flavors, Iterable):
|
||||||
|
raise ValueError("ldap_groups_filter: 'flavors' must be an iterable or comma-separated string")
|
||||||
|
|
||||||
|
have_gon = "groupOfNames" in flavors
|
||||||
|
have_ou = "organizationalUnit" in flavors
|
||||||
|
|
||||||
|
if have_gon and have_ou:
|
||||||
|
classes = ["groupOfNames", "organizationalUnit"]
|
||||||
|
return f"(|{''.join(f'(objectClass={c})' for c in classes)})"
|
||||||
|
if have_gon:
|
||||||
|
return "(objectClass=groupOfNames)"
|
||||||
|
if have_ou:
|
||||||
|
return "(objectClass=organizationalUnit)"
|
||||||
|
# fallback
|
||||||
|
return f"(objectClass={default})"
|
@@ -21,5 +21,3 @@ galaxy_info:
|
|||||||
class: "fa-solid fa-lock"
|
class: "fa-solid fa-lock"
|
||||||
run_after:
|
run_after:
|
||||||
- web-app-matomo
|
- web-app-matomo
|
||||||
dependencies:
|
|
||||||
- web-svc-logout
|
|
@@ -1,15 +0,0 @@
|
|||||||
- name: "load variables from {{ DOCKER_VARS_FILE }}"
|
|
||||||
include_vars: "{{ DOCKER_VARS_FILE }}"
|
|
||||||
|
|
||||||
- name: "create directory {{ KEYCLOAK_HOST_IMPORT_DIR }}"
|
|
||||||
file:
|
|
||||||
path: "{{ KEYCLOAK_HOST_IMPORT_DIR }}"
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: "Copy import files to {{ KEYCLOAK_HOST_IMPORT_DIR }}"
|
|
||||||
template:
|
|
||||||
src: "{{ item }}"
|
|
||||||
dest: "{{ KEYCLOAK_HOST_IMPORT_DIR }}/{{ item | basename | regex_replace('\\.j2$', '') }}"
|
|
||||||
mode: "0770"
|
|
||||||
loop: "{{ lookup('fileglob', role_path ~ '/templates/import/*.j2', wantlist=True) }}"
|
|
8
roles/web-app-keycloak/tasks/01_meta.yml
Normal file
8
roles/web-app-keycloak/tasks/01_meta.yml
Normal 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"
|
7
roles/web-app-keycloak/tasks/02_cleanup.yml
Normal file
7
roles/web-app-keycloak/tasks/02_cleanup.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
- ansible.builtin.include_vars:
|
||||||
|
file: "{{ playbook_dir }}/roles/docker-compose/vars/docker-compose.yml"
|
||||||
|
|
||||||
|
- name: "remove directory {{ KEYCLOAK_REALM_IMPORT_DIR_HOST }}"
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ KEYCLOAK_REALM_IMPORT_DIR_HOST }}"
|
||||||
|
state: absent
|
15
roles/web-app-keycloak/tasks/03_init.yml
Normal file
15
roles/web-app-keycloak/tasks/03_init.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
- name: "load variables from {{ DOCKER_VARS_FILE }}"
|
||||||
|
include_vars: "{{ DOCKER_VARS_FILE }}"
|
||||||
|
|
||||||
|
- name: "create directory {{ KEYCLOAK_REALM_IMPORT_DIR_HOST }}"
|
||||||
|
file:
|
||||||
|
path: "{{ KEYCLOAK_REALM_IMPORT_DIR_HOST }}"
|
||||||
|
state: directory
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: "Copy REALM import file '{{ KEYCLOAK_REALM_IMPORT_FILE_SRC }}' to '{{ KEYCLOAK_REALM_IMPORT_FILE_DST }}'"
|
||||||
|
template:
|
||||||
|
src: "{{ KEYCLOAK_REALM_IMPORT_FILE_SRC }}"
|
||||||
|
dest: "{{ KEYCLOAK_REALM_IMPORT_FILE_DST }}"
|
||||||
|
mode: "0770"
|
||||||
|
when: KEYCLOAK_REALM_IMPORT_ENABLED | bool
|
12
roles/web-app-keycloak/tasks/04_dependencies.yml
Normal file
12
roles/web-app-keycloak/tasks/04_dependencies.yml
Normal 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
|
74
roles/web-app-keycloak/tasks/05_rbac_client_scope.yml
Normal file
74
roles/web-app-keycloak/tasks/05_rbac_client_scope.yml
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# --- Ensure RBAC client scope exists (idempotent) ---
|
||||||
|
- name: Ensure RBAC client scope exists
|
||||||
|
shell: |
|
||||||
|
cat <<'JSON' | {{ KEYCLOAK_EXEC_KCADM }} create client-scopes -r {{ KEYCLOAK_REALM }} -f -
|
||||||
|
{{
|
||||||
|
(
|
||||||
|
KEYCLOAK_DICTIONARY_REALM.clientScopes
|
||||||
|
| selectattr('name','equalto', KEYCLOAK_RBAC_GROUP_CLAIM)
|
||||||
|
| list | first
|
||||||
|
) | to_json
|
||||||
|
}}
|
||||||
|
JSON
|
||||||
|
register: create_rbac_scope
|
||||||
|
changed_when: create_rbac_scope.rc == 0
|
||||||
|
failed_when: create_rbac_scope.rc != 0 and
|
||||||
|
('already exists' not in (create_rbac_scope.stderr | lower))
|
||||||
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||||
|
|
||||||
|
# --- Get the scope id we will attach to the client ---
|
||||||
|
- name: Get all client scopes
|
||||||
|
shell: "{{ KEYCLOAK_EXEC_KCADM }} get client-scopes -r {{ KEYCLOAK_REALM }} --format json"
|
||||||
|
register: all_scopes
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Extract RBAC scope id
|
||||||
|
set_fact:
|
||||||
|
scope_id_rbac: >-
|
||||||
|
{{ (
|
||||||
|
all_scopes.stdout | from_json
|
||||||
|
| selectattr('name','equalto', KEYCLOAK_RBAC_GROUP_CLAIM)
|
||||||
|
| list | first | default({})
|
||||||
|
).id | default('') }}
|
||||||
|
|
||||||
|
- name: Resolve application client id
|
||||||
|
shell: >
|
||||||
|
{{ KEYCLOAK_EXEC_KCADM }} get clients
|
||||||
|
-r {{ KEYCLOAK_REALM }}
|
||||||
|
--query 'clientId={{ KEYCLOAK_CLIENT_ID }}'
|
||||||
|
--fields id --format json | jq -r '.[0].id'
|
||||||
|
register: app_client_id_cmd
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Sanity check IDs
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- scope_id_rbac | length > 0
|
||||||
|
- (app_client_id_cmd.stdout | trim) is match('^[0-9a-f-]+$')
|
||||||
|
fail_msg: "Could not determine client or scope ID."
|
||||||
|
|
||||||
|
- name: Get current optional client scopes
|
||||||
|
shell: >
|
||||||
|
{{ KEYCLOAK_EXEC_KCADM }} get
|
||||||
|
clients/{{ app_client_id_cmd.stdout | trim }}/optional-client-scopes
|
||||||
|
-r {{ KEYCLOAK_REALM }} --format json
|
||||||
|
register: opt_scopes
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Decide if RBAC scope already assigned
|
||||||
|
set_fact:
|
||||||
|
has_rbac_optional: >-
|
||||||
|
{{ (opt_scopes.stdout | from_json
|
||||||
|
| selectattr('id','equalto', scope_id_rbac) | list | length) > 0 }}
|
||||||
|
|
||||||
|
- name: Ensure RBAC scope assigned as optional (only if missing)
|
||||||
|
when: not has_rbac_optional
|
||||||
|
shell: >
|
||||||
|
{{ KEYCLOAK_EXEC_KCADM }} update
|
||||||
|
clients/{{ app_client_id_cmd.stdout | trim }}/optional-client-scopes/{{ scope_id_rbac }}
|
||||||
|
-r {{ KEYCLOAK_REALM }}
|
||||||
|
register: add_opt
|
||||||
|
changed_when: true
|
||||||
|
failed_when: add_opt.rc != 0
|
||||||
|
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
|
||||||
|
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
|
144
roles/web-app-keycloak/tasks/06_ldap.yml
Normal file
144
roles/web-app-keycloak/tasks/06_ldap.yml
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
- name: "Update REALM settings (merge LDAP component .config)"
|
||||||
|
include_tasks: _update.yml
|
||||||
|
vars:
|
||||||
|
kc_object_kind: "component"
|
||||||
|
kc_lookup_value: "{{ KEYCLOAK_LDAP_CMP_NAME }}"
|
||||||
|
kc_desired: >-
|
||||||
|
{{
|
||||||
|
(
|
||||||
|
KEYCLOAK_DICTIONARY_REALM.components['org.keycloak.storage.UserStorageProvider']
|
||||||
|
| selectattr('name','equalto', KEYCLOAK_LDAP_CMP_NAME)
|
||||||
|
| list | first
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
kc_merge_path: "config"
|
||||||
|
|
||||||
|
- name: Resolve LDAP component id
|
||||||
|
shell: >
|
||||||
|
{{ KEYCLOAK_EXEC_KCADM }} get components
|
||||||
|
-r {{ KEYCLOAK_REALM }}
|
||||||
|
--query 'name={{ KEYCLOAK_LDAP_CMP_NAME }}'
|
||||||
|
--fields id --format json | jq -r '.[0].id'
|
||||||
|
register: ldap_cmp_id
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Assert LDAP component id resolved
|
||||||
|
assert:
|
||||||
|
that: [ "(ldap_cmp_id.stdout | trim) not in ['', 'null']" ]
|
||||||
|
fail_msg: "LDAP component '{{ KEYCLOAK_LDAP_CMP_NAME }}' not found in Keycloak."
|
||||||
|
|
||||||
|
- name: Pull LDAP component from dictionary (by name)
|
||||||
|
set_fact:
|
||||||
|
ldap_component_tpl: >-
|
||||||
|
{{
|
||||||
|
KEYCLOAK_DICTIONARY_REALM.components['org.keycloak.storage.UserStorageProvider']
|
||||||
|
| selectattr('name','equalto', KEYCLOAK_LDAP_CMP_NAME)
|
||||||
|
| list | first | default({})
|
||||||
|
}}
|
||||||
|
|
||||||
|
- name: Sanity check dictionary LDAP component
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- ldap_component_tpl | length > 0
|
||||||
|
- (ldap_component_tpl.subComponents | default({})) | length > 0
|
||||||
|
fail_msg: "LDAP component '{{ KEYCLOAK_LDAP_CMP_NAME }}' not found in KEYCLOAK_DICTIONARY_REALM."
|
||||||
|
|
||||||
|
- name: Extract mapper 'ldap-roles' from template (raw)
|
||||||
|
set_fact:
|
||||||
|
desired_group_mapper_raw: >-
|
||||||
|
{{
|
||||||
|
(
|
||||||
|
ldap_component_tpl.subComponents['org.keycloak.storage.ldap.mappers.LDAPStorageMapper']
|
||||||
|
| default([])
|
||||||
|
)
|
||||||
|
| selectattr('name','equalto','ldap-roles')
|
||||||
|
| list | first | default({})
|
||||||
|
}}
|
||||||
|
|
||||||
|
- name: Ensure mapper exists in dictionary
|
||||||
|
assert:
|
||||||
|
that: [ "desired_group_mapper_raw | length > 0" ]
|
||||||
|
fail_msg: "'ldap-roles' mapper not found in dictionary under LDAP component."
|
||||||
|
|
||||||
|
- name: Build clean mapper payload
|
||||||
|
set_fact:
|
||||||
|
desired_group_mapper: >-
|
||||||
|
{{
|
||||||
|
desired_group_mapper_raw
|
||||||
|
| dict2items
|
||||||
|
| rejectattr('key','in',['subComponents','id'])
|
||||||
|
| list | items2dict
|
||||||
|
}}
|
||||||
|
|
||||||
|
- name: Extract desired groups.path (if any)
|
||||||
|
set_fact:
|
||||||
|
desired_groups_path: "{{ (desired_group_mapper.config['groups.path'] | default([])) | first | default('') | trim }}"
|
||||||
|
desired_groups_top: "{{ ( (desired_group_mapper.config['groups.path'] | default([])) | first | default('') | trim ).lstrip('/') | split('/') | first | default('') }}"
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Resolve existing top-level group id for groups.path
|
||||||
|
when: desired_groups_top | length > 0
|
||||||
|
shell: >
|
||||||
|
{{ KEYCLOAK_EXEC_KCADM }} get groups -r {{ KEYCLOAK_REALM }} --format json
|
||||||
|
| jq -r '.[] | select(.name=="{{ desired_groups_top }}") | .id' | head -n1
|
||||||
|
register: groups_top_id
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Create top-level group for groups.path if missing
|
||||||
|
when:
|
||||||
|
- desired_groups_top | length > 0
|
||||||
|
- (groups_top_id.stdout | trim) in ["", "null"]
|
||||||
|
shell: >
|
||||||
|
{{ KEYCLOAK_EXEC_KCADM }} create groups -r {{ KEYCLOAK_REALM }} -s name={{ desired_groups_top }}
|
||||||
|
register: create_groups_top
|
||||||
|
changed_when: create_groups_top.rc == 0
|
||||||
|
failed_when: create_groups_top.rc != 0 and ('already exists' not in (create_groups_top.stderr | lower))
|
||||||
|
|
||||||
|
- name: Find existing 'ldap-roles' mapper under LDAP component
|
||||||
|
shell: >
|
||||||
|
{{ KEYCLOAK_EXEC_KCADM }} get components
|
||||||
|
-r {{ KEYCLOAK_REALM }}
|
||||||
|
--query "parent={{ ldap_cmp_id.stdout | trim }}&type=org.keycloak.storage.ldap.mappers.LDAPStorageMapper&name=ldap-roles"
|
||||||
|
--format json
|
||||||
|
| jq -r '.[] | select(.parentId=="{{ ldap_cmp_id.stdout | trim }}" and .name=="ldap-roles") | .id' | head -n1
|
||||||
|
register: grp_mapper_id
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Create 'ldap-roles' mapper if missing
|
||||||
|
when: (grp_mapper_id.stdout | trim) in ["", "null"]
|
||||||
|
shell: |
|
||||||
|
cat <<'JSON' | {{ KEYCLOAK_EXEC_KCADM }} create components -r {{ KEYCLOAK_REALM }} -f -
|
||||||
|
{{
|
||||||
|
desired_group_mapper
|
||||||
|
| combine({
|
||||||
|
'name': 'ldap-roles',
|
||||||
|
'parentId': ldap_cmp_id.stdout | trim,
|
||||||
|
'providerType': 'org.keycloak.storage.ldap.mappers.LDAPStorageMapper',
|
||||||
|
'providerId': 'group-ldap-mapper'
|
||||||
|
}, recursive=True)
|
||||||
|
| to_json
|
||||||
|
}}
|
||||||
|
JSON
|
||||||
|
register: create_mapper
|
||||||
|
changed_when: create_mapper.rc == 0
|
||||||
|
failed_when: create_mapper.rc != 0 and ('already exists' not in (create_mapper.stderr | lower))
|
||||||
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||||
|
|
||||||
|
- name: Update 'ldap-roles' mapper config (merge only .config)
|
||||||
|
when: (grp_mapper_id.stdout | trim) not in ["", "null"]
|
||||||
|
vars:
|
||||||
|
kc_object_kind: "component"
|
||||||
|
kc_lookup_field: "id"
|
||||||
|
kc_lookup_value: "{{ grp_mapper_id.stdout | trim }}"
|
||||||
|
kc_desired: >-
|
||||||
|
{{
|
||||||
|
desired_group_mapper
|
||||||
|
| combine({
|
||||||
|
'name': 'ldap-roles',
|
||||||
|
'parentId': ldap_cmp_id.stdout | trim,
|
||||||
|
'providerType': 'org.keycloak.storage.ldap.mappers.LDAPStorageMapper',
|
||||||
|
'providerId': 'group-ldap-mapper'
|
||||||
|
}, recursive=True)
|
||||||
|
}}
|
||||||
|
kc_merge_path: "config"
|
||||||
|
include_tasks: _update.yml
|
@@ -14,58 +14,100 @@
|
|||||||
- name: Assert required vars
|
- name: Assert required vars
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
- kc_object_kind in ['client','component']
|
- kc_object_kind in ['client','component','client-scope']
|
||||||
- kc_lookup_value is defined
|
- kc_lookup_value is defined
|
||||||
- kc_desired is defined
|
- kc_desired is defined
|
||||||
fail_msg: "kc_object_kind, kc_lookup_value, kc_desired are required."
|
fail_msg: "kc_object_kind, kc_lookup_value, kc_desired are required."
|
||||||
|
|
||||||
- name: Derive API endpoint and lookup field
|
- name: Derive API endpoint and lookup field
|
||||||
set_fact:
|
set_fact:
|
||||||
kc_api: "{{ 'clients' if kc_object_kind == 'client' else 'components' }}"
|
kc_api: >-
|
||||||
kc_lookup_field_eff: "{{ 'clientId' if kc_object_kind == 'client' else (kc_lookup_field | default('name')) }}"
|
{{ 'clients' if kc_object_kind == 'client'
|
||||||
|
else 'components' if kc_object_kind == 'component'
|
||||||
|
else 'client-scopes' if kc_object_kind == 'client-scope'
|
||||||
|
else '' }}
|
||||||
|
kc_lookup_field_eff: >-
|
||||||
|
{{ 'clientId' if kc_object_kind == 'client'
|
||||||
|
else (kc_lookup_field | default('name')) if kc_object_kind == 'component'
|
||||||
|
else 'name' if kc_object_kind == 'client-scope'
|
||||||
|
else '' }}
|
||||||
|
|
||||||
- name: Resolve object id
|
- name: Resolve object id (direct when lookup_field is id)
|
||||||
|
when: kc_lookup_field_eff == 'id'
|
||||||
|
set_fact:
|
||||||
|
kc_obj_id: "{{ kc_lookup_value | string }}"
|
||||||
|
|
||||||
|
- name: Resolve object id via query
|
||||||
|
when: kc_lookup_field_eff != 'id'
|
||||||
shell: >
|
shell: >
|
||||||
|
{% if kc_object_kind == 'client-scope' -%}
|
||||||
|
{{ KEYCLOAK_EXEC_KCADM }} get client-scopes -r {{ KEYCLOAK_REALM }} --format json
|
||||||
|
| jq -r '.[] | select(.{{ kc_lookup_field_eff }}=="{{ kc_lookup_value }}") | .id' | head -n1
|
||||||
|
{%- else -%}
|
||||||
{{ KEYCLOAK_EXEC_KCADM }} get {{ kc_api }}
|
{{ KEYCLOAK_EXEC_KCADM }} get {{ kc_api }}
|
||||||
-r {{ KEYCLOAK_REALM }}
|
-r {{ KEYCLOAK_REALM }}
|
||||||
--query '{{ kc_lookup_field_eff }}={{ kc_lookup_value }}'
|
--query '{{ kc_lookup_field_eff }}={{ kc_lookup_value }}'
|
||||||
--fields id --format json | jq -r '.[0].id'
|
--fields id --format json | jq -r '.[0].id'
|
||||||
register: kc_obj_id
|
{%- endif %}
|
||||||
|
register: kc_obj_id_cmd
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Normalize resolved id to a plain string
|
||||||
|
set_fact:
|
||||||
|
kc_obj_id: >-
|
||||||
|
{{
|
||||||
|
kc_obj_id
|
||||||
|
if kc_lookup_field_eff == 'id'
|
||||||
|
else (kc_obj_id_cmd.stdout | default('') | trim)
|
||||||
|
}}
|
||||||
|
|
||||||
- name: Fail if object not found
|
- name: Fail if object not found
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
- (kc_obj_id.stdout | trim) != ''
|
- (kc_obj_id | trim) != ''
|
||||||
- (kc_obj_id.stdout | trim) != 'null'
|
- (kc_obj_id | trim) != 'null'
|
||||||
fail_msg: "{{ kc_object_kind | capitalize }} '{{ kc_lookup_value }}' not found."
|
fail_msg: "{{ kc_object_kind | capitalize }} '{{ kc_lookup_value }}' not found."
|
||||||
|
|
||||||
- name: Read current object
|
- name: Read current object
|
||||||
shell: >
|
shell: >
|
||||||
{{ KEYCLOAK_EXEC_KCADM }} get {{ kc_api }}/{{ kc_obj_id.stdout }}
|
{{ KEYCLOAK_EXEC_KCADM }} get {{ kc_api }}/{{ kc_obj_id }}
|
||||||
-r {{ KEYCLOAK_REALM }} --format json
|
-r {{ KEYCLOAK_REALM }} --format json
|
||||||
register: kc_cur
|
register: kc_cur
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||||
# ── Build merge payload safely (avoid evaluating kc_desired[kc_merge_path] when undefined) ─────────
|
|
||||||
|
|
||||||
- name: Parse current object
|
- name: Parse current object
|
||||||
set_fact:
|
set_fact:
|
||||||
cur_obj: "{{ kc_cur.stdout | from_json }}"
|
cur_obj: "{{ kc_cur.stdout | from_json }}"
|
||||||
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||||
|
|
||||||
|
- name: "Safety check: providerId must match when updating a component"
|
||||||
|
when:
|
||||||
|
- kc_object_kind == 'component'
|
||||||
|
- (kc_desired.providerId is defined)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- cur_obj.providerId == kc_desired.providerId
|
||||||
|
fail_msg: >
|
||||||
|
Refusing to update component '{{ cur_obj.name }}' (providerId={{ cur_obj.providerId }})
|
||||||
|
because desired providerId={{ kc_desired.providerId }}. Check your lookup/ID.
|
||||||
|
|
||||||
- name: Prepare merge payload (subpath)
|
- name: Prepare merge payload (subpath)
|
||||||
when: kc_merge_path is defined and (kc_merge_path | length) > 0
|
when: kc_merge_path is defined and (kc_merge_path | length) > 0
|
||||||
set_fact:
|
set_fact:
|
||||||
merge_payload: "{{ { (kc_merge_path): (kc_desired[kc_merge_path] | default({}, true)) } }}"
|
merge_payload: "{{ { (kc_merge_path): (kc_desired[kc_merge_path] | default({}, true)) } }}"
|
||||||
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||||
|
|
||||||
- name: Prepare merge payload (full object)
|
- name: Prepare merge payload (full object)
|
||||||
when: kc_merge_path is not defined or (kc_merge_path | length) == 0
|
when: kc_merge_path is not defined or (kc_merge_path | length) == 0
|
||||||
set_fact:
|
set_fact:
|
||||||
merge_payload: "{{ kc_desired }}"
|
merge_payload: "{{ kc_desired }}"
|
||||||
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||||
|
|
||||||
- name: Build desired object (base merge)
|
- name: Build desired object (base merge)
|
||||||
set_fact:
|
set_fact:
|
||||||
desired_obj: "{{ cur_obj | combine(merge_payload, recursive=True) }}"
|
desired_obj: "{{ cur_obj | combine(merge_payload, recursive=True) }}"
|
||||||
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||||
|
|
||||||
# Preserve immutable fields
|
# Preserve immutable fields
|
||||||
- name: Preserve immutable fields for client
|
- name: Preserve immutable fields for client
|
||||||
@@ -79,6 +121,7 @@
|
|||||||
'clientId': cur_obj.clientId
|
'clientId': cur_obj.clientId
|
||||||
}, recursive=True)
|
}, recursive=True)
|
||||||
}}
|
}}
|
||||||
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||||
|
|
||||||
- name: Preserve immutable fields for component
|
- name: Preserve immutable fields for component
|
||||||
when: kc_object_kind == 'component'
|
when: kc_object_kind == 'component'
|
||||||
@@ -93,6 +136,14 @@
|
|||||||
'parentId': cur_obj.parentId
|
'parentId': cur_obj.parentId
|
||||||
}, recursive=True)
|
}, recursive=True)
|
||||||
}}
|
}}
|
||||||
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||||
|
|
||||||
|
# Preserve immutables for client-scope
|
||||||
|
- name: Preserve immutable fields for client-scope
|
||||||
|
when: kc_object_kind == 'client-scope'
|
||||||
|
set_fact:
|
||||||
|
desired_obj: "{{ desired_obj | combine({'id': cur_obj.id, 'name': cur_obj.name}, recursive=True) }}"
|
||||||
|
|
||||||
|
|
||||||
# Optional forced attributes (e.g., frontchannelLogout)
|
# Optional forced attributes (e.g., frontchannelLogout)
|
||||||
- name: Apply forced attributes (optional)
|
- name: Apply forced attributes (optional)
|
||||||
@@ -102,6 +153,8 @@
|
|||||||
|
|
||||||
- name: Update object via stdin
|
- name: Update object via stdin
|
||||||
shell: |
|
shell: |
|
||||||
cat <<'JSON' | {{ KEYCLOAK_EXEC_KCADM }} update {{ kc_api }}/{{ kc_obj_id.stdout }} -r {{ KEYCLOAK_REALM }} -f -
|
cat <<'JSON' | {{ KEYCLOAK_EXEC_KCADM }} update {{ kc_api }}/{{ kc_obj_id }} -r {{ KEYCLOAK_REALM }} -f -
|
||||||
{{ desired_obj | to_json }}
|
{{ desired_obj | to_json }}
|
||||||
JSON
|
JSON
|
||||||
|
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
|
||||||
|
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
|
@@ -1,31 +1,34 @@
|
|||||||
---
|
---
|
||||||
- name: "create import files for {{ application_id }}"
|
- name: "Load meta"
|
||||||
include_tasks: 01_initialize.yml
|
include_tasks: 01_meta.yml
|
||||||
|
when: not KEYCLOAK_LOAD_DEPENDENCIES | bool
|
||||||
|
|
||||||
- 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 }}"
|
- name: "Load cleanup routine for '{{ application_id }}'"
|
||||||
include_role:
|
include_tasks: 02_cleanup.yml
|
||||||
name: cmp-db-docker-proxy
|
|
||||||
vars:
|
|
||||||
docker_compose_flush_handlers: true
|
|
||||||
|
|
||||||
- name: "Wait until Keycloak is reachable at {{ KEYCLOAK_SERVER_HOST_URL }}"
|
- name: "Load init routine for '{{ application_id }}'"
|
||||||
uri:
|
include_tasks: 03_init.yml
|
||||||
url: "{{ KEYCLOAK_MASTER_REALM_URL }}"
|
|
||||||
method: GET
|
- name: "Load the depdendencies required by '{{ application_id }}'"
|
||||||
status_code: 200
|
include_tasks: 04_dependencies.yml
|
||||||
validate_certs: false
|
when: KEYCLOAK_LOAD_DEPENDENCIES | bool
|
||||||
register: kc_up
|
|
||||||
retries: 30
|
- name: "Wait until '{{ KEYCLOAK_CONTAINER }}' container is healthy"
|
||||||
|
community.docker.docker_container_info:
|
||||||
|
name: "{{ KEYCLOAK_CONTAINER }}"
|
||||||
|
register: kc_info
|
||||||
|
retries: 60
|
||||||
delay: 5
|
delay: 5
|
||||||
until: kc_up.status == 200
|
until: >
|
||||||
|
kc_info is succeeded and
|
||||||
|
(kc_info.container | default({})) != {} and
|
||||||
|
(kc_info.container.State | default({})) != {} and
|
||||||
|
(kc_info.container.State.Health | default({})) != {} and
|
||||||
|
(kc_info.container.State.Health.Status | default('')) == 'healthy'
|
||||||
|
|
||||||
- name: kcadm login (master)
|
- name: kcadm login (master)
|
||||||
no_log: true
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||||
shell: >
|
shell: >
|
||||||
{{ KEYCLOAK_EXEC_KCADM }} config credentials
|
{{ KEYCLOAK_EXEC_KCADM }} config credentials
|
||||||
--server {{ KEYCLOAK_SERVER_INTERNAL_URL }}
|
--server {{ KEYCLOAK_SERVER_INTERNAL_URL }}
|
||||||
@@ -34,19 +37,6 @@
|
|||||||
--password {{ KEYCLOAK_MASTER_API_USER_PASSWORD }}
|
--password {{ KEYCLOAK_MASTER_API_USER_PASSWORD }}
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|
||||||
- name: "Update REALM settings"
|
|
||||||
include_tasks: 02_update.yml
|
|
||||||
vars:
|
|
||||||
kc_object_kind: "component"
|
|
||||||
kc_lookup_value: "{{ KEYCLOAK_LDAP_CMP_NAME }}"
|
|
||||||
kc_desired: >-
|
|
||||||
{{
|
|
||||||
KEYCLOAK_DICTIONARY_REALM.components['org.keycloak.storage.UserStorageProvider']
|
|
||||||
| selectattr('providerId','equalto','ldap')
|
|
||||||
| list | first }}
|
|
||||||
kc_merge_path: "config"
|
|
||||||
when: KEYCLOAK_LDAP_ENABLED | bool
|
|
||||||
|
|
||||||
- name: "Update Client settings"
|
- name: "Update Client settings"
|
||||||
vars:
|
vars:
|
||||||
kc_object_kind: "client"
|
kc_object_kind: "client"
|
||||||
@@ -59,6 +49,16 @@
|
|||||||
}}
|
}}
|
||||||
kc_force_attrs:
|
kc_force_attrs:
|
||||||
frontchannelLogout: true
|
frontchannelLogout: true
|
||||||
attributes: "{{ (KEYCLOAK_DICTIONARY_CLIENT.attributes | default({}))
|
attributes: >-
|
||||||
| combine({'frontchannel.logout.url': KEYCLOAK_FRONTCHANNEL_LOGOUT_URL}, recursive=True) }}"
|
{{
|
||||||
include_tasks: 02_update.yml
|
( (KEYCLOAK_DICTIONARY_REALM.clients
|
||||||
|
| selectattr('clientId','equalto', KEYCLOAK_CLIENT_ID)
|
||||||
|
| list | first | default({}) ).attributes | default({}) )
|
||||||
|
| combine({'frontchannel.logout.url': KEYCLOAK_FRONTCHANNEL_LOGOUT_URL}, recursive=True)
|
||||||
|
}}
|
||||||
|
include_tasks: _update.yml
|
||||||
|
|
||||||
|
- include_tasks: 05_rbac_client_scope.yml
|
||||||
|
|
||||||
|
- include_tasks: 06_ldap.yml
|
||||||
|
when: KEYCLOAK_LDAP_ENABLED | bool
|
||||||
|
@@ -3,12 +3,12 @@
|
|||||||
application:
|
application:
|
||||||
image: "{{ KEYCLOAK_IMAGE }}:{{ KEYCLOAK_VERSION }}"
|
image: "{{ KEYCLOAK_IMAGE }}:{{ KEYCLOAK_VERSION }}"
|
||||||
container_name: {{ KEYCLOAK_CONTAINER }}
|
container_name: {{ KEYCLOAK_CONTAINER }}
|
||||||
command: start{% if KEYCLOAK_IMPORT_REALM_ENABLED %} --import-realm{% endif %}{% if KEYCLOAK_DEBUG_ENABLED %} --verbose{% endif %}
|
command: start{% if KEYCLOAK_REALM_IMPORT_ENABLED %} --import-realm{% endif %}{% if KEYCLOAK_DEBUG_ENABLED %} --verbose{% endif %}
|
||||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||||
ports:
|
ports:
|
||||||
- "{{ KEYCLOAK_SERVER_HOST }}:8080"
|
- "{{ KEYCLOAK_SERVER_HOST }}:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- "{{ KEYCLOAK_HOST_IMPORT_DIR }}:{{KEYCLOAK_DOCKER_IMPORT_DIR}}"
|
- "{{ KEYCLOAK_REALM_IMPORT_DIR_HOST }}:{{ KEYCLOAK_REALM_IMPORT_DIR_DOCKER }}"
|
||||||
{% include 'roles/docker-container/templates/depends_on/dmbs_excl.yml.j2' %}
|
{% include 'roles/docker-container/templates/depends_on/dmbs_excl.yml.j2' %}
|
||||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||||
{% set container_port = 9000 %}
|
{% set container_port = 9000 %}
|
||||||
|
@@ -7,7 +7,7 @@ KC_HTTP_ENABLED= true
|
|||||||
|
|
||||||
# Health Checks
|
# Health Checks
|
||||||
# @see https://quarkus.io/guides/smallrye-health
|
# @see https://quarkus.io/guides/smallrye-health
|
||||||
KC_HEALTH_ENABLED= true
|
KC_HEALTH_ENABLED= {{ KEYCLOAK_HEALTH_ENABLED | lower }}
|
||||||
KC_METRICS_ENABLED= true
|
KC_METRICS_ENABLED= true
|
||||||
|
|
||||||
# Administrator
|
# Administrator
|
||||||
|
@@ -28,6 +28,7 @@
|
|||||||
"oidc.ciba.grant.enabled": "false",
|
"oidc.ciba.grant.enabled": "false",
|
||||||
"client.secret.creation.time": "0",
|
"client.secret.creation.time": "0",
|
||||||
"backchannel.logout.session.required": "true",
|
"backchannel.logout.session.required": "true",
|
||||||
|
"standard.token.exchange.enabled": "false",
|
||||||
"post.logout.redirect.uris": {{ KEYCLOAK_POST_LOGOUT_URIS | to_json }},
|
"post.logout.redirect.uris": {{ KEYCLOAK_POST_LOGOUT_URIS | to_json }},
|
||||||
"frontchannel.logout.session.required": "true",
|
"frontchannel.logout.session.required": "true",
|
||||||
"oauth2.device.authorization.grant.enabled": "false",
|
"oauth2.device.authorization.grant.enabled": "false",
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
"organization",
|
"organization",
|
||||||
"offline_access",
|
"offline_access",
|
||||||
"microprofile-jwt",
|
"microprofile-jwt",
|
||||||
"{{ applications | get_app_conf(application_id, 'scopes.rbac_roles', True) }}",
|
"{{ KEYCLOAK_RBAC_GROUP_CLAIM }}",
|
||||||
"{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}"
|
"{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}"
|
||||||
]
|
]
|
||||||
}
|
}
|
@@ -1,9 +1,10 @@
|
|||||||
{
|
"org.keycloak.storage.UserStorageProvider": [
|
||||||
|
{
|
||||||
"name": "{{ KEYCLOAK_LDAP_CMP_NAME }}",
|
"name": "{{ KEYCLOAK_LDAP_CMP_NAME }}",
|
||||||
"providerId": "ldap",
|
"providerId": "ldap",
|
||||||
"subComponents": {
|
"subComponents": {
|
||||||
"org.keycloak.storage.ldap.mappers.LDAPStorageMapper": [
|
"org.keycloak.storage.ldap.mappers.LDAPStorageMapper": [
|
||||||
|
|
||||||
{# ---------------------- First Name ---------------------- #}
|
{# ---------------------- First Name ---------------------- #}
|
||||||
{
|
{
|
||||||
"name": "first name",
|
"name": "first name",
|
||||||
@@ -149,21 +150,52 @@
|
|||||||
"groups.dn": [ "{{ ldap.dn.ou.roles }}" ],
|
"groups.dn": [ "{{ ldap.dn.ou.roles }}" ],
|
||||||
"mode": [ "LDAP_ONLY" ],
|
"mode": [ "LDAP_ONLY" ],
|
||||||
"user.roles.retrieve.strategy": [ "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE" ],
|
"user.roles.retrieve.strategy": [ "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE" ],
|
||||||
"groups.ldap.filter": [
|
"groups.ldap.filter": ["{{ ldap.rbac.flavors | ldap_groups_filter }}"],
|
||||||
"{% set flavors = ldap.rbac.flavors | default([]) %}\
|
|
||||||
{% if 'groupOfNames' in flavors and 'organizationalUnit' in flavors %}(|(objectClass=groupOfNames)(objectClass=organizationalUnit))\
|
|
||||||
{% elif 'groupOfNames' in flavors %}(objectClass=groupOfNames)\
|
|
||||||
{% elif 'organizationalUnit' in flavors %}(objectClass=organizationalUnit)\
|
|
||||||
{% else %}(objectClass=groupOfNames){% endif %}"
|
|
||||||
],
|
|
||||||
"membership.ldap.attribute": [ "member" ],
|
"membership.ldap.attribute": [ "member" ],
|
||||||
"ignore.missing.groups": [ "true" ],
|
"ignore.missing.groups": [ "true" ],
|
||||||
"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 }}" ]
|
||||||
}
|
}
|
||||||
}{% if keycloak_map_ldap_realm_roles | default(false) %},
|
},
|
||||||
|
{
|
||||||
|
"name": "phone number",
|
||||||
|
"providerId": "user-attribute-ldap-mapper",
|
||||||
|
"subComponents": {},
|
||||||
|
"config": {
|
||||||
|
"ldap.attribute": [ "telephoneNumber" ],
|
||||||
|
"is.mandatory.in.ldap": [ "false" ],
|
||||||
|
"always.read.value.from.ldap": [ "true" ],
|
||||||
|
"read.only": [ "false" ],
|
||||||
|
"user.model.attribute": [ "phoneNumber" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "locale",
|
||||||
|
"providerId": "user-attribute-ldap-mapper",
|
||||||
|
"subComponents": {},
|
||||||
|
"config": {
|
||||||
|
"ldap.attribute": [ "preferredLanguage" ],
|
||||||
|
"is.mandatory.in.ldap": [ "false" ],
|
||||||
|
"always.read.value.from.ldap": [ "true" ],
|
||||||
|
"read.only": [ "false" ],
|
||||||
|
"user.model.attribute": [ "locale" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "uidNumber",
|
||||||
|
"providerId": "user-attribute-ldap-mapper",
|
||||||
|
"subComponents": {},
|
||||||
|
"config": {
|
||||||
|
"ldap.attribute": [ "uidNumber" ],
|
||||||
|
"is.mandatory.in.ldap": [ "false" ],
|
||||||
|
"always.read.value.from.ldap": [ "true" ],
|
||||||
|
"read.only": [ "false" ],
|
||||||
|
"user.model.attribute": [ "uidNumber" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% if keycloak_map_ldap_realm_roles | default(false) %},
|
||||||
{# ---------------------- LDAP -> Realm Roles (optional) -- #}
|
{# ---------------------- LDAP -> Realm Roles (optional) -- #}
|
||||||
{
|
{
|
||||||
"name": "ldap-realm-roles",
|
"name": "ldap-realm-roles",
|
||||||
@@ -182,7 +214,6 @@
|
|||||||
"role.object.classes": [ "groupOfNames" ]
|
"role.object.classes": [ "groupOfNames" ]
|
||||||
}
|
}
|
||||||
}{% endif %}
|
}{% endif %}
|
||||||
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
@@ -225,3 +256,4 @@
|
|||||||
"removeInvalidUsersEnabled": [ "true" ]
|
"removeInvalidUsersEnabled": [ "true" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
]
|
@@ -0,0 +1,61 @@
|
|||||||
|
{% set user_profile = {
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"name": "username",
|
||||||
|
"displayName": "${username}",
|
||||||
|
"validations": {"length": {"min": 3, "max": 255}, "pattern": {"pattern": "^[a-z0-9]+$", "error-message": ""}},
|
||||||
|
"annotations": {},
|
||||||
|
"permissions": {"view": ["admin","user"], "edit": ["admin","user"]},
|
||||||
|
"multivalued": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "email",
|
||||||
|
"displayName": "${email}",
|
||||||
|
"validations": {"email": {}, "length": {"max": 255}},
|
||||||
|
"required": {"roles": ["user"]},
|
||||||
|
"permissions": {"view": ["admin","user"], "edit": ["admin","user"]},
|
||||||
|
"multivalued": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "firstName",
|
||||||
|
"displayName": "${firstName}",
|
||||||
|
"validations": {"length": {"max": 255}, "person-name-prohibited-characters": {}},
|
||||||
|
"required": {"roles": ["user"]},
|
||||||
|
"permissions": {"view": ["admin","user"], "edit": ["admin","user"]},
|
||||||
|
"multivalued": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lastName",
|
||||||
|
"displayName": "${lastName}",
|
||||||
|
"validations": {"length": {"max": 255}, "person-name-prohibited-characters": {}},
|
||||||
|
"required": {"roles": ["user"]},
|
||||||
|
"permissions": {"view": ["admin","user"], "edit": ["admin","user"]},
|
||||||
|
"multivalued": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ldap.user.attributes.ssh_public_key,
|
||||||
|
"displayName": "SSH Public Key",
|
||||||
|
"validations": {},
|
||||||
|
"annotations": {},
|
||||||
|
"permissions": {"view": ["admin","user"], "edit": ["admin","user"]},
|
||||||
|
"group": "user-metadata",
|
||||||
|
"multivalued": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "user-metadata",
|
||||||
|
"displayHeader": "User metadata",
|
||||||
|
"displayDescription": "Attributes, which refer to user metadata"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} %}
|
||||||
|
"org.keycloak.userprofile.UserProfileProvider": [
|
||||||
|
{
|
||||||
|
"providerId": "declarative-user-profile",
|
||||||
|
"subComponents": {},
|
||||||
|
"config": {
|
||||||
|
"kc.user.profile.config": [{{ (user_profile | tojson) | tojson }}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@@ -507,7 +507,7 @@
|
|||||||
"fullScopeAllowed": false,
|
"fullScopeAllowed": false,
|
||||||
"nodeReRegistrationTimeout": 0,
|
"nodeReRegistrationTimeout": 0,
|
||||||
"defaultClientScopes": [
|
"defaultClientScopes": [
|
||||||
"web-app-origins",
|
"web-origins",
|
||||||
"acr",
|
"acr",
|
||||||
"roles",
|
"roles",
|
||||||
"profile",
|
"profile",
|
||||||
@@ -572,7 +572,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"defaultClientScopes": [
|
"defaultClientScopes": [
|
||||||
"web-app-origins",
|
"web-origins",
|
||||||
"acr",
|
"acr",
|
||||||
"roles",
|
"roles",
|
||||||
"profile",
|
"profile",
|
||||||
@@ -614,7 +614,7 @@
|
|||||||
"fullScopeAllowed": true,
|
"fullScopeAllowed": true,
|
||||||
"nodeReRegistrationTimeout": 0,
|
"nodeReRegistrationTimeout": 0,
|
||||||
"defaultClientScopes": [
|
"defaultClientScopes": [
|
||||||
"web-app-origins",
|
"web-origins",
|
||||||
"acr",
|
"acr",
|
||||||
"roles",
|
"roles",
|
||||||
"profile",
|
"profile",
|
||||||
@@ -655,7 +655,7 @@
|
|||||||
"fullScopeAllowed": false,
|
"fullScopeAllowed": false,
|
||||||
"nodeReRegistrationTimeout": 0,
|
"nodeReRegistrationTimeout": 0,
|
||||||
"defaultClientScopes": [
|
"defaultClientScopes": [
|
||||||
"web-app-origins",
|
"web-origins",
|
||||||
"acr",
|
"acr",
|
||||||
"roles",
|
"roles",
|
||||||
"profile",
|
"profile",
|
||||||
@@ -696,7 +696,7 @@
|
|||||||
"fullScopeAllowed": false,
|
"fullScopeAllowed": false,
|
||||||
"nodeReRegistrationTimeout": 0,
|
"nodeReRegistrationTimeout": 0,
|
||||||
"defaultClientScopes": [
|
"defaultClientScopes": [
|
||||||
"web-app-origins",
|
"web-origins",
|
||||||
"acr",
|
"acr",
|
||||||
"roles",
|
"roles",
|
||||||
"profile",
|
"profile",
|
||||||
@@ -763,7 +763,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"defaultClientScopes": [
|
"defaultClientScopes": [
|
||||||
"web-app-origins",
|
"web-origins",
|
||||||
"acr",
|
"acr",
|
||||||
"roles",
|
"roles",
|
||||||
"profile",
|
"profile",
|
||||||
@@ -778,7 +778,7 @@
|
|||||||
"microprofile-jwt"
|
"microprofile-jwt"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{% include "client.json.j2" %}
|
{% include "clients/default.json.j2" %}
|
||||||
],
|
],
|
||||||
"clientScopes": [
|
"clientScopes": [
|
||||||
{
|
{
|
||||||
@@ -1057,86 +1057,10 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{% include "scopes/rbac.json.j2" %},
|
||||||
|
{% include "scopes/nextcloud.json.j2" %},
|
||||||
{
|
{
|
||||||
"name": "{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}",
|
"name": "web-origins",
|
||||||
"description": "Optimized mappers for nextcloud oidc_login with ldap.",
|
|
||||||
"protocol": "openid-connect",
|
|
||||||
"attributes": {
|
|
||||||
"include.in.token.scope": "false",
|
|
||||||
"display.on.consent.screen": "true",
|
|
||||||
"gui.order": "",
|
|
||||||
"consent.screen.text": ""
|
|
||||||
},
|
|
||||||
"protocolMappers": [
|
|
||||||
{
|
|
||||||
"name": "{{ ldap.user.attributes.nextcloud_quota }}",
|
|
||||||
"protocol": "openid-connect",
|
|
||||||
"protocolMapper": "oidc-usermodel-attribute-mapper",
|
|
||||||
"consentRequired": false,
|
|
||||||
"config": {
|
|
||||||
"aggregate.attrs": "false",
|
|
||||||
"introspection.token.claim": "true",
|
|
||||||
"multivalued": "false",
|
|
||||||
"userinfo.token.claim": "true",
|
|
||||||
"user.attribute": "{{ ldap.user.attributes.nextcloud_quota }}",
|
|
||||||
"id.token.claim": "true",
|
|
||||||
"lightweight.claim": "false",
|
|
||||||
"access.token.claim": "true",
|
|
||||||
"claim.name": "{{ ldap.user.attributes.nextcloud_quota }}",
|
|
||||||
"jsonType.label": "int"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "UID Mapper",
|
|
||||||
"protocol": "openid-connect",
|
|
||||||
"protocolMapper": "oidc-usermodel-attribute-mapper",
|
|
||||||
"consentRequired": false,
|
|
||||||
"config": {
|
|
||||||
"aggregate.attrs": "false",
|
|
||||||
"introspection.token.claim": "true",
|
|
||||||
"multivalued": "false",
|
|
||||||
"userinfo.token.claim": "true",
|
|
||||||
"user.attribute": "username",
|
|
||||||
"id.token.claim": "true",
|
|
||||||
"lightweight.claim": "false",
|
|
||||||
"access.token.claim": "true",
|
|
||||||
"claim.name": "{{ldap.user.attributes.id}}",
|
|
||||||
"jsonType.label": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "{{ applications | get_app_conf(application_id, 'scopes.rbac_roles', True) }}",
|
|
||||||
"description": "RBAC Groups",
|
|
||||||
"protocol": "openid-connect",
|
|
||||||
"attributes": {
|
|
||||||
"include.in.token.scope": "false",
|
|
||||||
"display.on.consent.screen": "true",
|
|
||||||
"gui.order": "",
|
|
||||||
"consent.screen.text": ""
|
|
||||||
},
|
|
||||||
"protocolMappers": [
|
|
||||||
{
|
|
||||||
"name": "groups",
|
|
||||||
"protocol": "openid-connect",
|
|
||||||
"protocolMapper": "oidc-group-membership-mapper",
|
|
||||||
"consentRequired": false,
|
|
||||||
"config": {
|
|
||||||
"full.path": "true",
|
|
||||||
"introspection.token.claim": "true",
|
|
||||||
"userinfo.token.claim": "true",
|
|
||||||
"multivalued": "true",
|
|
||||||
"id.token.claim": "true",
|
|
||||||
"lightweight.claim": "false",
|
|
||||||
"access.token.claim": "true",
|
|
||||||
"claim.name": "{{ OIDC.CLAIMS.GROUPS }}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "web-app-origins",
|
|
||||||
"description": "OpenID Connect scope for add allowed web origins to the access token",
|
"description": "OpenID Connect scope for add allowed web origins to the access token",
|
||||||
"protocol": "openid-connect",
|
"protocol": "openid-connect",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
@@ -1496,7 +1420,7 @@
|
|||||||
"profile",
|
"profile",
|
||||||
"email",
|
"email",
|
||||||
"roles",
|
"roles",
|
||||||
"web-app-origins",
|
"web-origins",
|
||||||
"acr",
|
"acr",
|
||||||
"basic"
|
"basic"
|
||||||
],
|
],
|
||||||
@@ -1506,7 +1430,7 @@
|
|||||||
"phone",
|
"phone",
|
||||||
"microprofile-jwt",
|
"microprofile-jwt",
|
||||||
"organization",
|
"organization",
|
||||||
"{{ applications | get_app_conf(application_id, 'scopes.rbac_roles', True) }}",
|
"{{ KEYCLOAK_RBAC_GROUP_CLAIM }}",
|
||||||
"{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}"
|
"{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}"
|
||||||
],
|
],
|
||||||
"browserSecurityHeaders": {
|
"browserSecurityHeaders": {
|
||||||
@@ -1642,20 +1566,8 @@
|
|||||||
"config": {}
|
"config": {}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"org.keycloak.userprofile.UserProfileProvider": [
|
{%- include "components/org.keycloak.userprofile.UserProfileProvider.json.j2" -%},
|
||||||
{
|
{%- include "components/org.keycloak.storage.UserStorageProvider.json.j2" -%},
|
||||||
"providerId": "declarative-user-profile",
|
|
||||||
"subComponents": {},
|
|
||||||
"config": {
|
|
||||||
"kc.user.profile.config": [
|
|
||||||
"{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"pattern\":{\"pattern\":\"^[a-z0-9]+$\",\"error-message\":\"\"}},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"{{ ldap.user.attributes.ssh_public_key }}\",\"displayName\":\"SSH Public Key\",\"validations\":{},\"annotations\":{},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"group\":\"user-metadata\",\"multivalued\":true}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"org.keycloak.storage.UserStorageProvider": [
|
|
||||||
{% include "ldap.json.j2" %}
|
|
||||||
],
|
|
||||||
"org.keycloak.keys.KeyProvider": [
|
"org.keycloak.keys.KeyProvider": [
|
||||||
{
|
{
|
||||||
"name": "rsa-enc-generated",
|
"name": "rsa-enc-generated",
|
||||||
|
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "{{ applications | get_app_conf(application_id, 'scopes.nextcloud') }}",
|
||||||
|
"description": "Optimized mappers for nextcloud oidc_login with ldap.",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"attributes": {
|
||||||
|
"include.in.token.scope": "false",
|
||||||
|
"display.on.consent.screen": "true",
|
||||||
|
"gui.order": "",
|
||||||
|
"consent.screen.text": ""
|
||||||
|
},
|
||||||
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"name": "{{ ldap.user.attributes.nextcloud_quota }}",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-usermodel-attribute-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"aggregate.attrs": "false",
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"multivalued": "false",
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"user.attribute": "{{ ldap.user.attributes.nextcloud_quota }}",
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"lightweight.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "{{ ldap.user.attributes.nextcloud_quota }}",
|
||||||
|
"jsonType.label": "int"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "UID Mapper",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-usermodel-attribute-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"aggregate.attrs": "false",
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"multivalued": "false",
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"user.attribute": "username",
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"lightweight.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "{{ldap.user.attributes.id}}",
|
||||||
|
"jsonType.label": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
29
roles/web-app-keycloak/templates/import/scopes/rbac.json.j2
Normal file
29
roles/web-app-keycloak/templates/import/scopes/rbac.json.j2
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "{{ KEYCLOAK_RBAC_GROUP_CLAIM }}",
|
||||||
|
"description": "RBAC Groups",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"attributes": {
|
||||||
|
"include.in.token.scope": "false",
|
||||||
|
"display.on.consent.screen": "true",
|
||||||
|
"gui.order": "",
|
||||||
|
"consent.screen.text": ""
|
||||||
|
},
|
||||||
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"name": "groups",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-group-membership-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"full.path": "true",
|
||||||
|
"introspection.token.claim": "true",
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"multivalued": "true",
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"lightweight.claim": "false",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "{{ KEYCLOAK_RBAC_GROUP_CLAIM }}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@@ -10,28 +10,38 @@ KEYCLOAK_REALM: "{{ OIDC.CLIENT.REALM }}" # This is the name
|
|||||||
KEYCLOAK_REALM_URL: "{{ WEB_PROTOCOL }}://{{ KEYCLOAK_REALM }}"
|
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_MASTER_REALM_URL: "{{ KEYCLOAK_SERVER_HOST_URL }}/realms/master"
|
|
||||||
KEYCLOAK_HOST_IMPORT_DIR: "{{ docker_compose.directories.volumes }}import/"
|
|
||||||
KEYCLOAK_SERVER_INTERNAL_URL: "http://127.0.0.1:8080"
|
KEYCLOAK_SERVER_INTERNAL_URL: "http://127.0.0.1:8080"
|
||||||
|
KEYCLOAK_LOAD_DEPENDENCIES: "{{ applications | get_app_conf(application_id, 'load_dependencies') }}"
|
||||||
|
|
||||||
# Credentials
|
# RBAC
|
||||||
|
KEYCLOAK_RBAC_GROUP_CLAIM: "{{ RBAC.GROUP.CLAIM }}"
|
||||||
|
KEYCLOAK_RBAC_GROUP_NAME: "{{ RBAC.GROUP.NAME }}"
|
||||||
|
|
||||||
|
## Health
|
||||||
|
KEYCLOAK_HEALTH_ENABLED: true
|
||||||
|
|
||||||
|
## Import
|
||||||
|
KEYCLOAK_REALM_IMPORT_ENABLED: "{{ applications | get_app_conf(application_id, 'actions.import_realm') }}"
|
||||||
|
KEYCLOAK_REALM_IMPORT_DIR_HOST: "{{ docker_compose.directories.volumes }}import/"
|
||||||
|
KEYCLOAK_REALM_IMPORT_DIR_DOCKER: "/opt/keycloak/data/import/"
|
||||||
|
KEYCLOAK_REALM_IMPORT_FILE_SRC: "import/realm.json.j2"
|
||||||
|
KEYCLOAK_REALM_IMPORT_FILE_DST: "{{ KEYCLOAK_REALM_IMPORT_DIR_HOST }}/realm.json"
|
||||||
|
|
||||||
|
## Credentials
|
||||||
KEYCLOAK_ADMIN: "{{ applications | get_app_conf(application_id, 'users.administrator.username') }}"
|
KEYCLOAK_ADMIN: "{{ applications | get_app_conf(application_id, 'users.administrator.username') }}"
|
||||||
KEYCLOAK_ADMIN_PASSWORD: "{{ applications | get_app_conf(application_id, 'credentials.administrator_password') }}"
|
KEYCLOAK_ADMIN_PASSWORD: "{{ applications | get_app_conf(application_id, 'credentials.administrator_password') }}"
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
KEYCLOAK_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.name') }}" # Name of the keycloak docker container
|
KEYCLOAK_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.name') }}" # Name of the keycloak docker container
|
||||||
KEYCLOAK_DOCKER_IMPORT_DIR: "/opt/keycloak/data/import/" # Directory in which keycloak import files are placed in the running docker container
|
|
||||||
KEYCLOAK_EXEC_KCADM: "docker exec -i {{ KEYCLOAK_CONTAINER }} /opt/keycloak/bin/kcadm.sh" # Init script for keycloak
|
KEYCLOAK_EXEC_KCADM: "docker exec -i {{ KEYCLOAK_CONTAINER }} /opt/keycloak/bin/kcadm.sh" # Init script for keycloak
|
||||||
KEYCLOAK_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.image') }}" # Keycloak docker image
|
KEYCLOAK_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.image') }}" # Keycloak docker image
|
||||||
KEYCLOAK_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.version') }}" # Keycloak docker version
|
KEYCLOAK_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.keycloak.version') }}" # Keycloak docker version
|
||||||
|
|
||||||
## Server
|
## Server
|
||||||
KEYCLOAK_SERVER_HOST: "127.0.0.1:{{ ports.localhost.http[application_id] }}"
|
KEYCLOAK_SERVER_HOST: "127.0.0.1:{{ ports.localhost.http[application_id] }}"
|
||||||
KEYCLOAK_SERVER_HOST_URL: "http://{{ KEYCLOAK_SERVER_HOST }}"
|
|
||||||
|
|
||||||
## Update
|
## Update
|
||||||
KEYCLOAK_REDIRECT_FEATURES: ["features.oauth2","features.oidc"]
|
KEYCLOAK_REDIRECT_FEATURES: ["features.oauth2","features.oidc"]
|
||||||
KEYCLOAK_IMPORT_REALM_ENABLED: "{{ applications | get_app_conf(application_id, 'actions.import_realm') }}" # Activate realm import
|
|
||||||
KEYCLOAK_FRONTCHANNEL_LOGOUT_URL: "{{ domains | get_url('web-svc-logout', WEB_PROTOCOL) }}/"
|
KEYCLOAK_FRONTCHANNEL_LOGOUT_URL: "{{ domains | get_url('web-svc-logout', WEB_PROTOCOL) }}/"
|
||||||
KEYCLOAK_REDIRECT_URIS: "{{ domains | redirect_uris(applications, WEB_PROTOCOL, '/*', KEYCLOAK_REDIRECT_FEATURES) }}"
|
KEYCLOAK_REDIRECT_URIS: "{{ domains | redirect_uris(applications, WEB_PROTOCOL, '/*', KEYCLOAK_REDIRECT_FEATURES) }}"
|
||||||
KEYCLOAK_WEB_ORIGINS: >-
|
KEYCLOAK_WEB_ORIGINS: >-
|
||||||
@@ -54,7 +64,6 @@ KEYCLOAK_MASTER_API_USER: "{{ applications | get_app_conf(application_
|
|||||||
KEYCLOAK_MASTER_API_USER_NAME: "{{ KEYCLOAK_MASTER_API_USER.username }}" # Master Administrator Username
|
KEYCLOAK_MASTER_API_USER_NAME: "{{ KEYCLOAK_MASTER_API_USER.username }}" # Master Administrator Username
|
||||||
KEYCLOAK_MASTER_API_USER_PASSWORD: "{{ KEYCLOAK_MASTER_API_USER.password }}" # Master Administrator Password
|
KEYCLOAK_MASTER_API_USER_PASSWORD: "{{ KEYCLOAK_MASTER_API_USER.password }}" # Master Administrator Password
|
||||||
|
|
||||||
|
|
||||||
# Dictionaries
|
# Dictionaries
|
||||||
KEYCLOAK_DICTIONARY_REALM_RAW: "{{ lookup('template', 'import/realm.json.j2') }}"
|
KEYCLOAK_DICTIONARY_REALM_RAW: "{{ lookup('template', 'import/realm.json.j2') }}"
|
||||||
KEYCLOAK_DICTIONARY_REALM: >-
|
KEYCLOAK_DICTIONARY_REALM: >-
|
||||||
|
@@ -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
|
||||||
|
@@ -7,7 +7,7 @@ features:
|
|||||||
css: false
|
css: false
|
||||||
desktop: true
|
desktop: true
|
||||||
oidc: true
|
oidc: true
|
||||||
central_database: false # Deactivate central database for mailu, I don't know why the database deactivation is necessary
|
central_database: true
|
||||||
logout: true
|
logout: true
|
||||||
server:
|
server:
|
||||||
domains:
|
domains:
|
||||||
|
90
roles/web-app-mailu/docs/Move_Domain.md
Normal file
90
roles/web-app-mailu/docs/Move_Domain.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# 📖 **How to Migrate Mailboxes to a New Domain in Mailu**
|
||||||
|
|
||||||
|
When changing the primary email domain (e.g., from `cymais.cloud` to `infinito.nexus`), it is **not enough** to simply rename mailbox directories on disk. Mailu manages domain and user records in its internal database, and Dovecot maintains index files inside each Maildir. A blind rename will lead to login failures, rejected mail, or corrupted mail indices.
|
||||||
|
|
||||||
|
This guide explains the **safe procedure** for migrating user mailboxes to a new domain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 Why renaming folders directly does not work
|
||||||
|
|
||||||
|
* Mailu keeps **domains, users, and aliases** in the `admin_data` database. If you rename folders only, Mailu will not recognize the new accounts.
|
||||||
|
* Dovecot generates `.dovecot.index*` and `dovecot-uidlist` files in each mailbox. These must be rebuilt when moving mailboxes; otherwise, users may see missing or broken mail.
|
||||||
|
* Postfix will refuse to deliver or relay messages for unknown domains.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Correct migration procedure
|
||||||
|
|
||||||
|
### 1. Add the new domain and users in Mailu
|
||||||
|
|
||||||
|
Use the Mailu CLI inside the `admin` container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add new domain
|
||||||
|
docker compose exec admin flask mailu domain add infinito.nexus
|
||||||
|
|
||||||
|
# Add new user (repeat for each account)
|
||||||
|
docker compose exec admin flask mailu user kevinveenbirkenbach infinito.nexus 'NEW_PASSWORD'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Copy or move existing Maildir contents
|
||||||
|
|
||||||
|
Instead of renaming, copy the entire Maildir from the old domain to the new one:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rsync -aHAX --numeric-ids \
|
||||||
|
/var/lib/docker/volumes/mailu_dovecot_mail/_data/kevinveenbirkenbach@cymais.cloud/ \
|
||||||
|
/var/lib/docker/volumes/mailu_dovecot_mail/_data/kevinveenbirkenbach@infinito.nexus/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Remove old Dovecot index files
|
||||||
|
|
||||||
|
Ensure that Dovecot rebuilds indices cleanly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find /var/lib/docker/volumes/mailu_dovecot_mail/_data/kevinveenbirkenbach@infinito.nexus -type f -name '.dovecot*' -delete
|
||||||
|
find /var/lib/docker/volumes/mailu_dovecot_mail/_data/kevinveenbirkenbach@infinito.nexus -type f -name 'dovecot-uidlist*' -delete
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Fix file permissions
|
||||||
|
|
||||||
|
Make sure all mailbox files belong to the `mail:mail` user/group:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chown -R mail:mail /var/lib/docker/volumes/mailu_dovecot_mail/_data/kevinveenbirkenbach@infinito.nexus
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Restart services and test login
|
||||||
|
|
||||||
|
After copying, restart Mailu services (or at least `imap` and `smtp`) and confirm that users can log in with their new addresses.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. (Optional) Keep the old domain as an alias
|
||||||
|
|
||||||
|
To ensure incoming mail for the old domain is still accepted:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec admin flask mailu domain alias cymais.cloud infinito.nexus
|
||||||
|
```
|
||||||
|
|
||||||
|
This maps all `@cymais.cloud` addresses to their equivalents under `@infinito.nexus`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Summary
|
||||||
|
|
||||||
|
* Do **not** just rename mailbox folders.
|
||||||
|
* Always create the new domain and user in Mailu first.
|
||||||
|
* Copy existing Maildir contents into the new user’s directory.
|
||||||
|
* Remove Dovecot indices and fix permissions.
|
||||||
|
* Optionally configure a **domain alias** so old addresses remain valid.
|
@@ -38,20 +38,28 @@
|
|||||||
|
|
||||||
- name: "Create API token for '{{ mailu_user_key }};{{ mailu_user_name }}' if no local token defined"
|
- name: "Create API token for '{{ mailu_user_key }};{{ mailu_user_name }}' if no local token defined"
|
||||||
command: >-
|
command: >-
|
||||||
docker compose exec -T admin \
|
docker compose exec -T admin
|
||||||
curl -s -X POST {{ mailu_api_base_url }}/token \
|
curl -sS -f -X POST {{ mailu_api_base_url }}/token
|
||||||
-H "Authorization: Bearer {{ MAILU_API_TOKEN }}" \
|
-H "Authorization: Bearer {{ MAILU_API_TOKEN }}"
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json"
|
||||||
-d '{{ {
|
-d '{{ {
|
||||||
"comment": mailu_user_key ~ " - ansible.infinito",
|
"comment": mailu_user_key ~ " - ansible.infinito",
|
||||||
"email": users[mailu_user_key].email,
|
"email": users[mailu_user_key].email,
|
||||||
"ip": mailu_token_ip
|
"ip": mailu_token_ip
|
||||||
} | to_json }}'
|
} | to_json }}'
|
||||||
args:
|
args:
|
||||||
chdir: "{{ MAILU_DOCKER_DIR }}"
|
chdir: "{{ MAILU_DOCKER_DIR }}"
|
||||||
when: users[mailu_user_key].mailu_token is not defined
|
when: users[mailu_user_key].mailu_token is not defined
|
||||||
register: mailu_token_creation
|
register: mailu_token_creation
|
||||||
changed_when: mailu_token_creation.rc == 0
|
# If curl sees 4xx/5xx it returns non-zero due to -f → fail the task.
|
||||||
|
failed_when:
|
||||||
|
- mailu_token_creation.rc != 0
|
||||||
|
# Fallback: if some gateway returns 200 but embeds an error JSON.
|
||||||
|
- mailu_token_creation.rc == 0 and
|
||||||
|
(mailu_token_creation.stdout is search('"code"\\s*:\\s*4\\d\\d') or
|
||||||
|
mailu_token_creation.stdout is search('cannot be found'))
|
||||||
|
# Only mark changed when a token is actually present in the JSON.
|
||||||
|
changed_when: mailu_token_creation.stdout is search('"token"\\s*:')
|
||||||
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||||
|
|
||||||
- name: "Set mailu_token for '{{ mailu_user_key }};{{ mailu_user_name }}' in users dict if newly created"
|
- name: "Set mailu_token for '{{ mailu_user_key }};{{ mailu_user_name }}' in users dict if newly created"
|
||||||
|
@@ -23,7 +23,7 @@
|
|||||||
- "{{ MAILU_IP4_PUBLIC }}:993:993"
|
- "{{ MAILU_IP4_PUBLIC }}:993:993"
|
||||||
- "{{ MAILU_IP4_PUBLIC }}:4190:4190"
|
- "{{ MAILU_IP4_PUBLIC }}:4190:4190"
|
||||||
volumes:
|
volumes:
|
||||||
- "{{docker_compose.directories.volumes}}overrides/nginx:/overrides:ro"
|
- "{{ docker_compose.directories.volumes }}overrides/nginx:/overrides:ro"
|
||||||
- "{{ cert_mount_directory }}:/certs:ro"
|
- "{{ cert_mount_directory }}:/certs:ro"
|
||||||
{% include 'roles/docker-container/templates/depends_on/dmbs_incl.yml.j2' %}
|
{% include 'roles/docker-container/templates/depends_on/dmbs_incl.yml.j2' %}
|
||||||
resolver:
|
resolver:
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||||
volumes:
|
volumes:
|
||||||
- "dovecot_mail:/mail"
|
- "dovecot_mail:/mail"
|
||||||
- "{{docker_compose.directories.volumes}}overrides:/overrides:ro"
|
- "{{ docker_compose.directories.volumes }}overrides:/overrides:ro"
|
||||||
depends_on:
|
depends_on:
|
||||||
- front
|
- front
|
||||||
- resolver
|
- resolver
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
image: {{ MAILU_DOCKER_FLAVOR }}/postfix:{{ MAILU_VERSION }}
|
image: {{ MAILU_DOCKER_FLAVOR }}/postfix:{{ MAILU_VERSION }}
|
||||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||||
volumes:
|
volumes:
|
||||||
- "{{docker_compose.directories.volumes}}overrides:/overrides:ro"
|
- "{{ docker_compose.directories.volumes }}overrides:/overrides:ro"
|
||||||
- "smtp_queue:/queue"
|
- "smtp_queue:/queue"
|
||||||
depends_on:
|
depends_on:
|
||||||
- front
|
- front
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
volumes:
|
volumes:
|
||||||
- "filter:/var/lib/rspamd"
|
- "filter:/var/lib/rspamd"
|
||||||
- "dkim:/dkim"
|
- "dkim:/dkim"
|
||||||
- "{{docker_compose.directories.volumes}}overrides/rspamd:/overrides:ro"
|
- "{{ docker_compose.directories.volumes }}overrides/rspamd:/overrides:ro"
|
||||||
depends_on:
|
depends_on:
|
||||||
- front
|
- front
|
||||||
- redis
|
- redis
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||||
volumes:
|
volumes:
|
||||||
- "webmail_data:/data"
|
- "webmail_data:/data"
|
||||||
- "{{docker_compose.directories.volumes}}overrides:/overrides:ro"
|
- "{{ docker_compose.directories.volumes }}overrides:/overrides:ro"
|
||||||
depends_on:
|
depends_on:
|
||||||
- imap
|
- imap
|
||||||
- front
|
- front
|
||||||
|
@@ -136,7 +136,7 @@ REAL_IP_FROM=
|
|||||||
REJECT_UNLISTED_RECIPIENT=
|
REJECT_UNLISTED_RECIPIENT=
|
||||||
|
|
||||||
# Log level threshold in start.py (value: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET)
|
# Log level threshold in start.py (value: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET)
|
||||||
LOG_LEVEL=WARNING
|
LOG_LEVEL={{ 'DEBUG' if MODE_DEBUG else 'WARNING' }}
|
||||||
|
|
||||||
###################################
|
###################################
|
||||||
# Database settings
|
# Database settings
|
||||||
|
@@ -41,7 +41,7 @@
|
|||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:{{ports.localhost.http[application_id]}}:{{ container_port }}"
|
- "127.0.0.1:{{ports.localhost.http[application_id]}}:{{ container_port }}"
|
||||||
volumes:
|
volumes:
|
||||||
- "{{docker_compose.directories.volumes}}nginx.conf:/etc/nginx/nginx.conf:ro"
|
- "{{ docker_compose.directories.volumes }}nginx.conf:/etc/nginx/nginx.conf:ro"
|
||||||
volumes_from:
|
volumes_from:
|
||||||
- application
|
- application
|
||||||
|
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
ports:
|
ports:
|
||||||
- {{ports.localhost.oauth2_proxy[application_id]}}:4180/tcp
|
- {{ports.localhost.oauth2_proxy[application_id]}}:4180/tcp
|
||||||
volumes:
|
volumes:
|
||||||
- "{{docker_compose.directories.volumes}}{{applications | get_app_conf('web-app-oauth2-proxy','configuration_file')}}:/oauth2-proxy.cfg"
|
- "{{ docker_compose.directories.volumes }}{{applications | get_app_conf('web-app-oauth2-proxy','configuration_file')}}:/oauth2-proxy.cfg"
|
||||||
{% endif %}
|
{% endif %}
|
@@ -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 %}
|
||||||
|
@@ -29,12 +29,12 @@ openproject_cache_version: "{{ applications
|
|||||||
}}"
|
}}"
|
||||||
|
|
||||||
|
|
||||||
openproject_plugins_folder: "{{docker_compose.directories.volumes}}plugins/"
|
openproject_plugins_folder: "{{ docker_compose.directories.volumes }}plugins/"
|
||||||
|
|
||||||
openproject_custom_image: "custom_openproject"
|
openproject_custom_image: "custom_openproject"
|
||||||
|
|
||||||
# The following volume doesn't have a practcical function. It just exist to prevent the creation of unnecessary anonymous volumes
|
# The following volume doesn't have a practcical function. It just exist to prevent the creation of unnecessary anonymous volumes
|
||||||
openproject_dummy_volume: "{{docker_compose.directories.volumes}}dummy_volume"
|
openproject_dummy_volume: "{{ docker_compose.directories.volumes }}dummy_volume"
|
||||||
|
|
||||||
openproject_rails_settings:
|
openproject_rails_settings:
|
||||||
email_delivery_method: "smtp"
|
email_delivery_method: "smtp"
|
||||||
|
@@ -5,14 +5,14 @@ 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
|
||||||
desktop: true
|
desktop: true
|
||||||
central_database: true
|
central_database: true
|
||||||
oauth2: true
|
oauth2: true
|
||||||
logout: true
|
logout: true
|
||||||
server:
|
server:
|
||||||
csp:
|
csp:
|
||||||
flags:
|
flags:
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
pgadmin_host_server_file: "{{docker_compose.directories.volumes}}servers.json"
|
pgadmin_host_server_file: "{{ docker_compose.directories.volumes }}servers.json"
|
||||||
pgadmin_docker_server_file: "/pgadmin4/servers.json"
|
pgadmin_docker_server_file: "/pgadmin4/servers.json"
|
||||||
pgadmin_host_password_file: "{{docker_compose.directories.volumes}}.pgpass"
|
pgadmin_host_password_file: "{{ docker_compose.directories.volumes }}.pgpass"
|
||||||
pgadmin_docker_password_file: "/pgpass"
|
pgadmin_docker_password_file: "/pgpass"
|
||||||
|
|
||||||
pgadmin_servers:
|
pgadmin_servers:
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: true
|
css: true
|
||||||
desktop: false
|
desktop: false
|
||||||
simpleicons: true # Activate Brand Icons for your groups
|
simpleicons: true # Activate Brand Icons for your groups
|
||||||
javascript: true # Necessary for URL sync
|
javascript: true # Necessary for URL sync
|
||||||
logout: false # Doesn't have own user data. Just a frame.
|
logout: false # Doesn't have own user data. Just a frame.
|
||||||
server:
|
server:
|
||||||
csp:
|
csp:
|
||||||
whitelist:
|
whitelist:
|
||||||
|
@@ -9,20 +9,20 @@ cards:
|
|||||||
{% include 'menu/support.yml.j2' %}
|
{% include 'menu/support.yml.j2' %}
|
||||||
|
|
||||||
platform:
|
platform:
|
||||||
titel: {{service_provider.platform.titel}}
|
titel: {{ service_provider.platform.titel }}
|
||||||
subtitel: {{service_provider.platform.subtitel}}
|
subtitel: {{ service_provider.platform.subtitel }}
|
||||||
logo:
|
logo:
|
||||||
source: {{service_provider.platform.logo}}
|
source: {{ service_provider.platform.logo }}
|
||||||
favicon:
|
favicon:
|
||||||
source: {{service_provider.platform.favicon}}
|
source: {{ service_provider.platform.favicon }}
|
||||||
company:
|
company:
|
||||||
titel: {{service_provider.company.titel}}
|
titel: {{ service_provider.company.titel }}
|
||||||
subtitel: {{service_provider.company.slogan}}
|
subtitel: {{ service_provider.company.slogan }}
|
||||||
logo:
|
logo:
|
||||||
source: {{service_provider.company.logo}}
|
source: {{ service_provider.company.logo }}
|
||||||
address:
|
address:
|
||||||
{{ service_provider.company.address | to_nice_yaml(indent=4) | indent(4, true) }}
|
{{ service_provider.company.address | to_nice_yaml(indent=4) | indent(4, true) }}
|
||||||
imprint_url: {{service_provider.legal.imprint}}
|
imprint_url: {{ service_provider.legal.imprint }}
|
||||||
navigation:
|
navigation:
|
||||||
|
|
||||||
{% include 'menu/header.yml.j2' %}
|
{% include 'menu/header.yml.j2' %}
|
||||||
|
@@ -25,7 +25,7 @@ applications:
|
|||||||
url: {{ app.url }}
|
url: {{ app.url }}
|
||||||
iframe: {{ app.iframe }}
|
iframe: {{ app.iframe }}
|
||||||
{% if app.title == 'Keycloak' %}
|
{% if app.title == 'Keycloak' %}
|
||||||
{% set keycloak_url = domains | get_url(application_id, WEB_PROTOCOL) %}
|
{% set keycloak_url = domains | get_url('web-app-keycloak', WEB_PROTOCOL) %}
|
||||||
{{ domains | get_url(application_id, WEB_PROTOCOL) }}
|
{{ domains | get_url(application_id, WEB_PROTOCOL) }}
|
||||||
children:
|
children:
|
||||||
- name: Administration
|
- name: Administration
|
||||||
|
@@ -44,5 +44,5 @@
|
|||||||
description: Check out the imprint information
|
description: Check out the imprint information
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-scale-balanced
|
class: fa-solid fa-scale-balanced
|
||||||
url: "{{service_provider.legal.imprint}}"
|
url: "{{ service_provider.legal.imprint }}"
|
||||||
iframe: true
|
iframe: true
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user