mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-11-04 04:08:15 +00:00 
			
		
		
		
	Added parameter to skipp dependency loading to speed up debugging
This commit is contained in:
		@@ -1,3 +1,4 @@
 | 
			
		||||
load_dependencies:    True  # When set to false the dependencies aren't loaded. Helpful for developing
 | 
			
		||||
actions:
 | 
			
		||||
  import_realm:       True     # Import REALM
 | 
			
		||||
features:
 | 
			
		||||
@@ -29,7 +30,6 @@ server:
 | 
			
		||||
    canonical:
 | 
			
		||||
      - "auth.{{ PRIMARY_DOMAIN }}"
 | 
			
		||||
scopes:
 | 
			
		||||
  rbac_roles: rbac_roles
 | 
			
		||||
  nextcloud:  nextcloud
 | 
			
		||||
 | 
			
		||||
rbac_groups:  "/rbac"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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"
 | 
			
		||||
  run_after:
 | 
			
		||||
    - web-app-matomo
 | 
			
		||||
dependencies:
 | 
			
		||||
  - web-svc-logout
 | 
			
		||||
							
								
								
									
										4
									
								
								roles/web-app-keycloak/tasks/01_cleanup.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								roles/web-app-keycloak/tasks/01_cleanup.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
- name: "remove directory {{ KEYCLOAK_REALM_IMPORT_DIR_HOST }}"
 | 
			
		||||
  ansible.builtin.file:
 | 
			
		||||
    path: "{{ KEYCLOAK_REALM_IMPORT_DIR_HOST }}"
 | 
			
		||||
    state: absent
 | 
			
		||||
@@ -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) }}"
 | 
			
		||||
							
								
								
									
										15
									
								
								roles/web-app-keycloak/tasks/02_initialize.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								roles/web-app-keycloak/tasks/02_initialize.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
 | 
			
		||||
@@ -14,58 +14,100 @@
 | 
			
		||||
- name: Assert required vars
 | 
			
		||||
  assert:
 | 
			
		||||
    that:
 | 
			
		||||
      - kc_object_kind in ['client','component']
 | 
			
		||||
      - kc_object_kind in ['client','component','client-scope']
 | 
			
		||||
      - kc_lookup_value is defined
 | 
			
		||||
      - kc_desired is defined
 | 
			
		||||
    fail_msg: "kc_object_kind, kc_lookup_value, kc_desired are required."
 | 
			
		||||
 | 
			
		||||
- name: Derive API endpoint and lookup field
 | 
			
		||||
  set_fact:
 | 
			
		||||
    kc_api: "{{ 'clients' if kc_object_kind == 'client' else 'components' }}"
 | 
			
		||||
    kc_lookup_field_eff: "{{ 'clientId' if kc_object_kind == 'client' else (kc_lookup_field | default('name')) }}"
 | 
			
		||||
    kc_api: >-
 | 
			
		||||
      {{ '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: >
 | 
			
		||||
    {% 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 }}
 | 
			
		||||
    -r {{ KEYCLOAK_REALM }}
 | 
			
		||||
    --query '{{ kc_lookup_field_eff }}={{ kc_lookup_value }}'
 | 
			
		||||
    --fields id --format json | jq -r '.[0].id'
 | 
			
		||||
  register: kc_obj_id
 | 
			
		||||
    {%- endif %}
 | 
			
		||||
  register: kc_obj_id_cmd
 | 
			
		||||
  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
 | 
			
		||||
  assert:
 | 
			
		||||
    that:
 | 
			
		||||
      - (kc_obj_id.stdout | trim) != ''
 | 
			
		||||
      - (kc_obj_id.stdout | trim) != 'null'
 | 
			
		||||
      - (kc_obj_id | trim) != ''
 | 
			
		||||
      - (kc_obj_id | trim) != 'null'
 | 
			
		||||
    fail_msg: "{{ kc_object_kind | capitalize }} '{{ kc_lookup_value }}' not found."
 | 
			
		||||
 | 
			
		||||
- name: Read current object
 | 
			
		||||
  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
 | 
			
		||||
  register: kc_cur
 | 
			
		||||
  changed_when: false
 | 
			
		||||
 | 
			
		||||
# ── Build merge payload safely (avoid evaluating kc_desired[kc_merge_path] when undefined) ─────────
 | 
			
		||||
  no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
 | 
			
		||||
 | 
			
		||||
- name: Parse current object
 | 
			
		||||
  set_fact:
 | 
			
		||||
    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)
 | 
			
		||||
  when: kc_merge_path is defined and (kc_merge_path | length) > 0
 | 
			
		||||
  set_fact:
 | 
			
		||||
    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)
 | 
			
		||||
  when: kc_merge_path is not defined or (kc_merge_path | length) == 0
 | 
			
		||||
  set_fact:
 | 
			
		||||
    merge_payload: "{{ kc_desired }}"
 | 
			
		||||
  no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
 | 
			
		||||
 | 
			
		||||
- name: Build desired object (base merge)
 | 
			
		||||
  set_fact:
 | 
			
		||||
    desired_obj: "{{ cur_obj | combine(merge_payload, recursive=True) }}"
 | 
			
		||||
  no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
 | 
			
		||||
 | 
			
		||||
# Preserve immutable fields
 | 
			
		||||
- name: Preserve immutable fields for client
 | 
			
		||||
@@ -79,6 +121,7 @@
 | 
			
		||||
            'clientId': cur_obj.clientId
 | 
			
		||||
          }, recursive=True)
 | 
			
		||||
      }}
 | 
			
		||||
  no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
 | 
			
		||||
 | 
			
		||||
- name: Preserve immutable fields for component
 | 
			
		||||
  when: kc_object_kind == 'component'
 | 
			
		||||
@@ -93,6 +136,14 @@
 | 
			
		||||
            'parentId': cur_obj.parentId
 | 
			
		||||
          }, 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)
 | 
			
		||||
- name: Apply forced attributes (optional)
 | 
			
		||||
@@ -102,6 +153,8 @@
 | 
			
		||||
 | 
			
		||||
- name: Update object via stdin
 | 
			
		||||
  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 }}
 | 
			
		||||
    JSON
 | 
			
		||||
  async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
 | 
			
		||||
  poll:  "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
 | 
			
		||||
							
								
								
									
										72
									
								
								roles/web-app-keycloak/tasks/04_rbac_client_scope.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								roles/web-app-keycloak/tasks/04_rbac_client_scope.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
			
		||||
# --- 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_OIDC_RBAC_SCOPE_NAME)
 | 
			
		||||
        | 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_OIDC_RBAC_SCOPE_NAME)
 | 
			
		||||
          | 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
 | 
			
		||||
							
								
								
									
										134
									
								
								roles/web-app-keycloak/tasks/05_ldap.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								roles/web-app-keycloak/tasks/05_ldap.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,134 @@
 | 
			
		||||
# roles/web-app-keycloak/tasks/05_ldap.yml
 | 
			
		||||
---
 | 
			
		||||
- name: "Update REALM settings (merge LDAP component .config)"
 | 
			
		||||
  include_tasks: 03_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"
 | 
			
		||||
 | 
			
		||||
# --- Read desired mapper definition from KEYCLOAK_DICTIONARY_REALM ---
 | 
			
		||||
 | 
			
		||||
- name: Get LDAP component (from KEYCLOAK_DICTIONARY_REALM)
 | 
			
		||||
  set_fact:
 | 
			
		||||
    ldap_component: >-
 | 
			
		||||
      {{
 | 
			
		||||
        KEYCLOAK_DICTIONARY_REALM.components['org.keycloak.storage.UserStorageProvider']
 | 
			
		||||
        | selectattr('providerId','equalto','ldap')
 | 
			
		||||
        | list | first | default({})
 | 
			
		||||
      }}
 | 
			
		||||
 | 
			
		||||
- name: Sanity check LDAP component
 | 
			
		||||
  assert:
 | 
			
		||||
    that:
 | 
			
		||||
      - ldap_component | length > 0
 | 
			
		||||
      - (ldap_component.subComponents | default({})) | length > 0
 | 
			
		||||
    fail_msg: "LDAP component not found in KEYCLOAK_DICTIONARY_REALM."
 | 
			
		||||
 | 
			
		||||
- name: Extract desired group-ldap-mapper definition (raw)
 | 
			
		||||
  set_fact:
 | 
			
		||||
    desired_group_mapper_raw: >-
 | 
			
		||||
      {{
 | 
			
		||||
        (
 | 
			
		||||
          ldap_component.subComponents['org.keycloak.storage.ldap.mappers.LDAPStorageMapper']
 | 
			
		||||
          | default([])
 | 
			
		||||
        )
 | 
			
		||||
        | selectattr('providerId','equalto','group-ldap-mapper')
 | 
			
		||||
        | list | first | default({})
 | 
			
		||||
      }}
 | 
			
		||||
 | 
			
		||||
- name: Ensure we found the mapper in the dictionary
 | 
			
		||||
  assert:
 | 
			
		||||
    that:
 | 
			
		||||
      - desired_group_mapper_raw | length > 0
 | 
			
		||||
    fail_msg: "group-ldap-mapper not found below LDAP component in KEYCLOAK_DICTIONARY_REALM."
 | 
			
		||||
 | 
			
		||||
- name: Compute desired mapper name
 | 
			
		||||
  set_fact:
 | 
			
		||||
    desired_group_mapper_name: "{{ desired_group_mapper_raw.name | default('ldap-roles') }}"
 | 
			
		||||
 | 
			
		||||
- name: Build clean mapper payload (strip unsupported keys)
 | 
			
		||||
  set_fact:
 | 
			
		||||
    desired_group_mapper: >-
 | 
			
		||||
      {{
 | 
			
		||||
        desired_group_mapper_raw
 | 
			
		||||
        | dict2items
 | 
			
		||||
        | rejectattr('key','equalto','subComponents')
 | 
			
		||||
        | rejectattr('key','equalto','id')
 | 
			
		||||
        | list | items2dict
 | 
			
		||||
      }}
 | 
			
		||||
 | 
			
		||||
# --- Work against Keycloak ---
 | 
			
		||||
 | 
			
		||||
- 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: Check for group-ldap-mapper existence (by name 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={{ desired_group_mapper_name }}"
 | 
			
		||||
    --format json
 | 
			
		||||
    | jq -r '.[] | select(.parentId=="{{ ldap_cmp_id.stdout | trim }}"
 | 
			
		||||
           and .providerType=="org.keycloak.storage.ldap.mappers.LDAPStorageMapper"
 | 
			
		||||
           and .providerId=="group-ldap-mapper"
 | 
			
		||||
           and .name=="{{ desired_group_mapper_name }}") | .id' | head -n1
 | 
			
		||||
  register: grp_mapper_id
 | 
			
		||||
  changed_when: false
 | 
			
		||||
 | 
			
		||||
- name: Ensure group-ldap-mapper exists (create 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':        desired_group_mapper_name,
 | 
			
		||||
          '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 existing group-ldap-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':         desired_group_mapper_name,
 | 
			
		||||
            '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: 03_update.yml
 | 
			
		||||
@@ -1,31 +1,28 @@
 | 
			
		||||
---
 | 
			
		||||
- name: "create import files for {{ application_id }}"
 | 
			
		||||
  include_tasks: 01_initialize.yml
 | 
			
		||||
- name: "Load cleanup routine for '{{ application_id }}'"
 | 
			
		||||
  include_tasks: 01_cleanup.yml
 | 
			
		||||
 | 
			
		||||
- 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 init routine for '{{ application_id }}'"
 | 
			
		||||
  include_tasks: 02_initialize.yml
 | 
			
		||||
 | 
			
		||||
- name: "load docker, db and proxy for {{ application_id }}"
 | 
			
		||||
  include_role: 
 | 
			
		||||
    name: cmp-db-docker-proxy
 | 
			
		||||
  vars:
 | 
			
		||||
    docker_compose_flush_handlers: true
 | 
			
		||||
- name: "Load the depdendencies required by '{{ application_id }}'"
 | 
			
		||||
  include_tasks: 03_load_dependencies.yml
 | 
			
		||||
 | 
			
		||||
- name: "Wait until Keycloak is reachable at {{ KEYCLOAK_SERVER_HOST_URL }}"
 | 
			
		||||
  uri:
 | 
			
		||||
    url: "{{ KEYCLOAK_MASTER_REALM_URL }}"
 | 
			
		||||
    method: GET
 | 
			
		||||
    status_code: 200
 | 
			
		||||
    validate_certs: false
 | 
			
		||||
  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
 | 
			
		||||
  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)
 | 
			
		||||
  no_log: true
 | 
			
		||||
  no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
 | 
			
		||||
  shell: >
 | 
			
		||||
    {{ KEYCLOAK_EXEC_KCADM }} config credentials
 | 
			
		||||
    --server {{ KEYCLOAK_SERVER_INTERNAL_URL }}
 | 
			
		||||
@@ -34,19 +31,6 @@
 | 
			
		||||
    --password {{ KEYCLOAK_MASTER_API_USER_PASSWORD }}
 | 
			
		||||
  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"
 | 
			
		||||
  vars:
 | 
			
		||||
    kc_object_kind:  "client"
 | 
			
		||||
@@ -59,6 +43,16 @@
 | 
			
		||||
      }}
 | 
			
		||||
    kc_force_attrs:
 | 
			
		||||
      frontchannelLogout: true
 | 
			
		||||
      attributes: "{{ (KEYCLOAK_DICTIONARY_CLIENT.attributes | default({}))
 | 
			
		||||
                      | combine({'frontchannel.logout.url': KEYCLOAK_FRONTCHANNEL_LOGOUT_URL}, recursive=True) }}"
 | 
			
		||||
  include_tasks: 02_update.yml
 | 
			
		||||
      attributes: >-
 | 
			
		||||
        {{
 | 
			
		||||
          ( (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: 03_update.yml
 | 
			
		||||
 | 
			
		||||
- include_tasks: 04_rbac_client_scope.yml
 | 
			
		||||
 | 
			
		||||
- include_tasks: 05_ldap.yml
 | 
			
		||||
  when: KEYCLOAK_LDAP_ENABLED | bool
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,12 @@
 | 
			
		||||
  application:
 | 
			
		||||
    image: "{{ KEYCLOAK_IMAGE }}:{{ KEYCLOAK_VERSION }}"
 | 
			
		||||
    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' %}
 | 
			
		||||
    ports:
 | 
			
		||||
      - "{{ KEYCLOAK_SERVER_HOST }}:8080"
 | 
			
		||||
    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/networks.yml.j2' %}
 | 
			
		||||
{% set container_port = 9000 %}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ KC_HTTP_ENABLED=                true
 | 
			
		||||
 | 
			
		||||
# Health Checks
 | 
			
		||||
# @see https://quarkus.io/guides/smallrye-health
 | 
			
		||||
KC_HEALTH_ENABLED=              true
 | 
			
		||||
KC_HEALTH_ENABLED=              {{ KEYCLOAK_HEALTH_ENABLED | lower }}
 | 
			
		||||
KC_METRICS_ENABLED=             true
 | 
			
		||||
 | 
			
		||||
# Administrator
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,7 @@
 | 
			
		||||
    "oidc.ciba.grant.enabled": "false",
 | 
			
		||||
    "client.secret.creation.time": "0",
 | 
			
		||||
    "backchannel.logout.session.required": "true",
 | 
			
		||||
    "standard.token.exchange.enabled": "false",
 | 
			
		||||
    "post.logout.redirect.uris": {{ KEYCLOAK_POST_LOGOUT_URIS | to_json }},
 | 
			
		||||
    "frontchannel.logout.session.required": "true",
 | 
			
		||||
    "oauth2.device.authorization.grant.enabled": "false",
 | 
			
		||||
@@ -53,7 +54,7 @@
 | 
			
		||||
    "organization",
 | 
			
		||||
    "offline_access",
 | 
			
		||||
    "microprofile-jwt",
 | 
			
		||||
    "{{ applications | get_app_conf(application_id, 'scopes.rbac_roles', True) }}",
 | 
			
		||||
    "{{ KEYCLOAK_OIDC_RBAC_SCOPE_NAME }}",
 | 
			
		||||
    "{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
{
 | 
			
		||||
"org.keycloak.storage.UserStorageProvider": [
 | 
			
		||||
  {
 | 
			
		||||
  "name": "{{ KEYCLOAK_LDAP_CMP_NAME }}",
 | 
			
		||||
  "providerId": "ldap",
 | 
			
		||||
  "subComponents": {
 | 
			
		||||
    "org.keycloak.storage.ldap.mappers.LDAPStorageMapper": [
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
      {# ---------------------- First Name ---------------------- #}
 | 
			
		||||
      {
 | 
			
		||||
        "name": "first name",
 | 
			
		||||
@@ -149,13 +150,7 @@
 | 
			
		||||
          "groups.dn": [ "{{ ldap.dn.ou.roles }}" ],
 | 
			
		||||
          "mode": [ "LDAP_ONLY" ],
 | 
			
		||||
          "user.roles.retrieve.strategy": [ "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE" ],
 | 
			
		||||
          "groups.ldap.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 %}"
 | 
			
		||||
          ],
 | 
			
		||||
          "groups.ldap.filter": ["{{ ldap.rbac.flavors | ldap_groups_filter }}"],
 | 
			
		||||
          "membership.ldap.attribute": [ "member" ],
 | 
			
		||||
          "ignore.missing.groups": [ "true" ],
 | 
			
		||||
          "group.object.classes": [ "groupOfNames" ],
 | 
			
		||||
@@ -163,7 +158,44 @@
 | 
			
		||||
          "drop.non.existing.groups.during.sync": [ "false" ],
 | 
			
		||||
          "groups.path": [ "{{ applications | get_app_conf(application_id, 'rbac_groups', True) }}" ]
 | 
			
		||||
        }
 | 
			
		||||
      }{% 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) -- #}
 | 
			
		||||
      {
 | 
			
		||||
        "name": "ldap-realm-roles",
 | 
			
		||||
@@ -182,7 +214,6 @@
 | 
			
		||||
          "role.object.classes": [ "groupOfNames" ]
 | 
			
		||||
        }
 | 
			
		||||
      }{% endif %}
 | 
			
		||||
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "config": {
 | 
			
		||||
@@ -225,3 +256,4 @@
 | 
			
		||||
    "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,
 | 
			
		||||
      "nodeReRegistrationTimeout": 0,
 | 
			
		||||
      "defaultClientScopes": [
 | 
			
		||||
        "web-app-origins",
 | 
			
		||||
        "web-origins",
 | 
			
		||||
        "acr",
 | 
			
		||||
        "roles",
 | 
			
		||||
        "profile",
 | 
			
		||||
@@ -572,7 +572,7 @@
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "defaultClientScopes": [
 | 
			
		||||
        "web-app-origins",
 | 
			
		||||
        "web-origins",
 | 
			
		||||
        "acr",
 | 
			
		||||
        "roles",
 | 
			
		||||
        "profile",
 | 
			
		||||
@@ -614,7 +614,7 @@
 | 
			
		||||
      "fullScopeAllowed": true,
 | 
			
		||||
      "nodeReRegistrationTimeout": 0,
 | 
			
		||||
      "defaultClientScopes": [
 | 
			
		||||
        "web-app-origins",
 | 
			
		||||
        "web-origins",
 | 
			
		||||
        "acr",
 | 
			
		||||
        "roles",
 | 
			
		||||
        "profile",
 | 
			
		||||
@@ -655,7 +655,7 @@
 | 
			
		||||
      "fullScopeAllowed": false,
 | 
			
		||||
      "nodeReRegistrationTimeout": 0,
 | 
			
		||||
      "defaultClientScopes": [
 | 
			
		||||
        "web-app-origins",
 | 
			
		||||
        "web-origins",
 | 
			
		||||
        "acr",
 | 
			
		||||
        "roles",
 | 
			
		||||
        "profile",
 | 
			
		||||
@@ -696,7 +696,7 @@
 | 
			
		||||
      "fullScopeAllowed": false,
 | 
			
		||||
      "nodeReRegistrationTimeout": 0,
 | 
			
		||||
      "defaultClientScopes": [
 | 
			
		||||
        "web-app-origins",
 | 
			
		||||
        "web-origins",
 | 
			
		||||
        "acr",
 | 
			
		||||
        "roles",
 | 
			
		||||
        "profile",
 | 
			
		||||
@@ -763,7 +763,7 @@
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "defaultClientScopes": [
 | 
			
		||||
        "web-app-origins",
 | 
			
		||||
        "web-origins",
 | 
			
		||||
        "acr",
 | 
			
		||||
        "roles",
 | 
			
		||||
        "profile",
 | 
			
		||||
@@ -778,7 +778,7 @@
 | 
			
		||||
        "microprofile-jwt"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    {% include "client.json.j2" %}
 | 
			
		||||
    {% include "clients/default.json.j2" %}
 | 
			
		||||
  ],
 | 
			
		||||
  "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) }}",
 | 
			
		||||
      "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",
 | 
			
		||||
      "name": "web-origins",
 | 
			
		||||
      "description": "OpenID Connect scope for add allowed web origins to the access token",
 | 
			
		||||
      "protocol": "openid-connect",
 | 
			
		||||
      "attributes": {
 | 
			
		||||
@@ -1496,7 +1420,7 @@
 | 
			
		||||
    "profile",
 | 
			
		||||
    "email",
 | 
			
		||||
    "roles",
 | 
			
		||||
    "web-app-origins",
 | 
			
		||||
    "web-origins",
 | 
			
		||||
    "acr",
 | 
			
		||||
    "basic"
 | 
			
		||||
  ],
 | 
			
		||||
@@ -1506,7 +1430,7 @@
 | 
			
		||||
    "phone",
 | 
			
		||||
    "microprofile-jwt",
 | 
			
		||||
    "organization",
 | 
			
		||||
    "{{ applications | get_app_conf(application_id, 'scopes.rbac_roles', True) }}",
 | 
			
		||||
    "{{ KEYCLOAK_OIDC_RBAC_SCOPE_NAME }}",
 | 
			
		||||
    "{{ applications | get_app_conf(application_id, 'scopes.nextcloud', True) }}"
 | 
			
		||||
  ],
 | 
			
		||||
  "browserSecurityHeaders": {
 | 
			
		||||
@@ -1642,20 +1566,8 @@
 | 
			
		||||
        "config": {}
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "org.keycloak.userprofile.UserProfileProvider": [
 | 
			
		||||
      {
 | 
			
		||||
        "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" %}
 | 
			
		||||
    ],
 | 
			
		||||
    {%- include "components/org.keycloak.userprofile.UserProfileProvider.json.j2" -%},
 | 
			
		||||
    {%- include "components/org.keycloak.storage.UserStorageProvider.json.j2" -%},
 | 
			
		||||
    "org.keycloak.keys.KeyProvider": [
 | 
			
		||||
      {
 | 
			
		||||
        "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_OIDC_RBAC_SCOPE_NAME }}",
 | 
			
		||||
  "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 }}"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -10,28 +10,35 @@ KEYCLOAK_REALM:                     "{{ OIDC.CLIENT.REALM }}" # This is the name
 | 
			
		||||
KEYCLOAK_REALM_URL:                 "{{ WEB_PROTOCOL }}://{{ KEYCLOAK_REALM }}"
 | 
			
		||||
KEYCLOAK_DEBUG_ENABLED:             "{{ MODE_DEBUG }}"
 | 
			
		||||
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_OIDC_RBAC_SCOPE_NAME:      "{{ OIDC.CLAIMS.GROUPS }}"
 | 
			
		||||
KEYCLOAK_LOAD_DEPENDENCIES:         "{{ applications | get_app_conf(application_id, 'load_dependencies') }}"
 | 
			
		||||
 | 
			
		||||
# Credentials
 | 
			
		||||
## 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_PASSWORD:            "{{ applications | get_app_conf(application_id, 'credentials.administrator_password') }}"
 | 
			
		||||
 | 
			
		||||
## Docker
 | 
			
		||||
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_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
 | 
			
		||||
 | 
			
		||||
## Server
 | 
			
		||||
KEYCLOAK_SERVER_HOST:               "127.0.0.1:{{ ports.localhost.http[application_id] }}"
 | 
			
		||||
KEYCLOAK_SERVER_HOST_URL:           "http://{{ KEYCLOAK_SERVER_HOST }}"
 | 
			
		||||
  
 | 
			
		||||
## Update
 | 
			
		||||
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_REDIRECT_URIS:             "{{ domains | redirect_uris(applications, WEB_PROTOCOL, '/*', KEYCLOAK_REDIRECT_FEATURES) }}"
 | 
			
		||||
KEYCLOAK_WEB_ORIGINS: >-
 | 
			
		||||
@@ -54,7 +61,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_PASSWORD:  "{{ KEYCLOAK_MASTER_API_USER.password }}"                                  # Master Administrator Password
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Dictionaries
 | 
			
		||||
KEYCLOAK_DICTIONARY_REALM_RAW: "{{ lookup('template', 'import/realm.json.j2') }}"
 | 
			
		||||
KEYCLOAK_DICTIONARY_REALM: >-
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user