mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-10-31 02:10:05 +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