From 3fcaebe1a88a50946117c655a210372b34a8c864 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 27 Feb 2026 11:22:08 +0100 Subject: [PATCH 01/39] feat: add keycloak provisioning tasks --- roles/keycloak/defaults/main.yml | 63 +++++++ roles/keycloak/tasks/main.yml | 22 +++ roles/keycloak/tasks/provisioning.yml | 156 ++++++++++++++++++ .../keycloak/templates/docker-compose.yml.j2 | 1 + 4 files changed, 242 insertions(+) create mode 100644 roles/keycloak/tasks/provisioning.yml diff --git a/roles/keycloak/defaults/main.yml b/roles/keycloak/defaults/main.yml index 66d0a72..c242ea5 100644 --- a/roles/keycloak/defaults/main.yml +++ b/roles/keycloak/defaults/main.yml @@ -33,3 +33,66 @@ keycloak_use_ssl: true keycloak_log_level: "INFO" keycloak_proxy_mode: "edge" keycloak_gzip_enabled: false # Disable GZIP encoding to avoid MIME type issues + +# Provisioning configuration +keycloak_provisioning_enabled: false + +# Realm configuration +keycloak_realm: "default" +keycloak_realm_display_name: "Default Realm" + +# Auth URL for API access (used by provisioning tasks) +keycloak_auth_url: "{{ 'https' if keycloak_use_ssl else 'http' }}://{{ keycloak_domain }}" + +# Groups to provision +keycloak_groups: [] +# - name: admins +# - name: users + +# Local users to provision +keycloak_local_users: [] +# - username: admin +# first_name: "Admin" +# last_name: "User" +# email: "admin@example.com" +# password: "changeme" +# groups: +# - name: admins + +# OIDC clients to provision +keycloak_oidc_clients: [] +# - client_id: nextcloud +# name: "Nextcloud" +# client_secret: "changeme" +# redirect_uris: +# - "https://nextcloud.example.com/apps/user_oidc/code" +# default_client_scopes: +# - openid +# - email +# - profile + +# Identity providers (e.g., Entra ID, Google) +keycloak_identity_providers: [] +# - alias: entra-id +# display_name: "Login with Microsoft" +# provider_id: oidc +# config: +# clientId: "{{ entra_client_id }}" +# clientSecret: "{{ entra_client_secret }}" +# authorizationUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" +# tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token" +# defaultScope: "openid profile email" + +# Resources to remove from Keycloak (cleanup) +# Add names/aliases here when removing from the lists above +keycloak_removed_users: [] +# - olduser + +keycloak_removed_groups: [] +# - oldgroup + +keycloak_removed_clients: [] +# - old-client + +keycloak_removed_identity_providers: [] +# - old-idp diff --git a/roles/keycloak/tasks/main.yml b/roles/keycloak/tasks/main.yml index 05db2ef..f8a0f1e 100644 --- a/roles/keycloak/tasks/main.yml +++ b/roles/keycloak/tasks/main.yml @@ -30,3 +30,25 @@ community.docker.docker_compose_v2: project_src: "{{ keycloak_docker_compose_dir }}" state: present + +- name: Wait for Keycloak health endpoint + uri: + url: "{{ keycloak_auth_url }}/health/ready" + method: GET + status_code: 200 + validate_certs: false + register: keycloak_health + until: keycloak_health.status == 200 + retries: 30 + delay: 10 + delegate_to: localhost + become: false + when: keycloak_provisioning_enabled | bool + +- name: Run Keycloak provisioning + ansible.builtin.include_tasks: provisioning.yml + args: + apply: + become: false + delegate_to: localhost + when: keycloak_provisioning_enabled | bool diff --git a/roles/keycloak/tasks/provisioning.yml b/roles/keycloak/tasks/provisioning.yml new file mode 100644 index 0000000..03ad6df --- /dev/null +++ b/roles/keycloak/tasks/provisioning.yml @@ -0,0 +1,156 @@ +#SPDX-License-Identifier: MIT-0 +--- +# Keycloak provisioning tasks +# Create realm (if not master) +- name: Create Keycloak realm + community.general.keycloak_realm: + auth_keycloak_url: "{{ keycloak_auth_url }}" + auth_realm: master + auth_username: "{{ keycloak_admin_user }}" + auth_password: "{{ keycloak_admin_password }}" + realm: "{{ keycloak_realm }}" + display_name: "{{ keycloak_realm_display_name }}" + enabled: true + state: present + validate_certs: false + no_log: true + when: keycloak_realm != "master" + +# Cleanup: Remove deleted identity providers +- name: Remove deleted identity providers + community.general.keycloak_identity_provider: + auth_keycloak_url: "{{ keycloak_auth_url }}" + auth_realm: master + auth_username: "{{ keycloak_admin_user }}" + auth_password: "{{ keycloak_admin_password }}" + realm: "{{ keycloak_realm }}" + alias: "{{ item }}" + state: absent + validate_certs: false + loop: "{{ keycloak_removed_identity_providers }}" + no_log: true + +# Cleanup: Remove deleted clients +- name: Remove deleted clients + community.general.keycloak_client: + auth_keycloak_url: "{{ keycloak_auth_url }}" + auth_realm: master + auth_username: "{{ keycloak_admin_user }}" + auth_password: "{{ keycloak_admin_password }}" + realm: "{{ keycloak_realm }}" + client_id: "{{ item }}" + state: absent + validate_certs: false + loop: "{{ keycloak_removed_clients }}" + no_log: true + +# Cleanup: Remove deleted users +- name: Remove deleted users + community.general.keycloak_user: + auth_keycloak_url: "{{ keycloak_auth_url }}" + auth_realm: master + auth_username: "{{ keycloak_admin_user }}" + auth_password: "{{ keycloak_admin_password }}" + realm: "{{ keycloak_realm }}" + username: "{{ item }}" + state: absent + validate_certs: false + loop: "{{ keycloak_removed_users }}" + no_log: true + +# Cleanup: Remove deleted groups +- name: Remove deleted groups + community.general.keycloak_group: + auth_keycloak_url: "{{ keycloak_auth_url }}" + auth_realm: master + auth_username: "{{ keycloak_admin_user }}" + auth_password: "{{ keycloak_admin_password }}" + realm: "{{ keycloak_realm }}" + name: "{{ item }}" + state: absent + validate_certs: false + loop: "{{ keycloak_removed_groups }}" + no_log: true + +# Create groups +- name: Create groups + community.general.keycloak_group: + auth_keycloak_url: "{{ keycloak_auth_url }}" + auth_realm: master + auth_username: "{{ keycloak_admin_user }}" + auth_password: "{{ keycloak_admin_password }}" + realm: "{{ keycloak_realm }}" + name: "{{ item.name }}" + state: present + validate_certs: false + loop: "{{ keycloak_groups }}" + no_log: true + +# Create local users +- name: Create local users + community.general.keycloak_user: + auth_keycloak_url: "{{ keycloak_auth_url }}" + auth_realm: master + auth_username: "{{ keycloak_admin_user }}" + auth_password: "{{ keycloak_admin_password }}" + realm: "{{ keycloak_realm }}" + username: "{{ item.username }}" + first_name: "{{ item.first_name | default(omit) }}" + last_name: "{{ item.last_name | default(omit) }}" + email: "{{ item.email | default(omit) }}" + enabled: "{{ item.enabled | default(true) }}" + email_verified: "{{ item.email_verified | default(true) }}" + credentials: + - type: password + value: "{{ item.password }}" + temporary: false + groups: "{{ item.groups | default([]) }}" + state: present + validate_certs: false + loop: "{{ keycloak_local_users }}" + no_log: true + +# Create OIDC clients +- name: Create OIDC clients + community.general.keycloak_client: + auth_keycloak_url: "{{ keycloak_auth_url }}" + auth_realm: master + auth_username: "{{ keycloak_admin_user }}" + auth_password: "{{ keycloak_admin_password }}" + realm: "{{ keycloak_realm }}" + client_id: "{{ item.client_id }}" + name: "{{ item.name | default(item.client_id) }}" + enabled: true + client_authenticator_type: client-secret + secret: "{{ item.client_secret }}" + redirect_uris: "{{ item.redirect_uris | default([]) }}" + web_origins: "{{ item.web_origins | default(['+']) }}" + standard_flow_enabled: true + implicit_flow_enabled: false + direct_access_grants_enabled: "{{ item.direct_access_grants_enabled | default(false) }}" + protocol: openid-connect + default_client_scopes: "{{ item.default_client_scopes | default(['openid', 'email', 'profile']) }}" + state: present + validate_certs: false + loop: "{{ keycloak_oidc_clients }}" + no_log: true + +# Create identity providers +- name: Create identity providers + community.general.keycloak_identity_provider: + auth_keycloak_url: "{{ keycloak_auth_url }}" + auth_realm: master + auth_username: "{{ keycloak_admin_user }}" + auth_password: "{{ keycloak_admin_password }}" + realm: "{{ keycloak_realm }}" + alias: "{{ item.alias }}" + display_name: "{{ item.display_name | default(item.alias) }}" + provider_id: "{{ item.provider_id }}" + enabled: "{{ item.enabled | default(true) }}" + trust_email: "{{ item.trust_email | default(true) }}" + first_broker_login_flow_alias: "{{ item.first_broker_login_flow_alias | default('first broker login') }}" + config: "{{ item.config }}" + state: present + validate_certs: false + loop: "{{ keycloak_identity_providers }}" + no_log: true \ No newline at end of file diff --git a/roles/keycloak/templates/docker-compose.yml.j2 b/roles/keycloak/templates/docker-compose.yml.j2 index a91f746..2708f37 100644 --- a/roles/keycloak/templates/docker-compose.yml.j2 +++ b/roles/keycloak/templates/docker-compose.yml.j2 @@ -32,6 +32,7 @@ services: KC_SPI_RESOURCE_ENCODING_GZIP_CACHE_DIR: /opt/keycloak/data/gzip-cache KC_PROXY: {{ keycloak_proxy_mode }} KC_HOSTNAME: {{ keycloak_domain }} + KC_HEALTH_ENABLED: "true" depends_on: - postgres volumes: From b5a6573beb7ced55eccacd332a3a0228c9aa6ff2 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 27 Feb 2026 11:23:07 +0100 Subject: [PATCH 02/39] feat: add nextcloud oidc provisioning --- roles/nextcloud/defaults/main.yml | 25 ++++++++- roles/nextcloud/tasks/main.yml | 12 +++++ roles/nextcloud/tasks/oidc.yml | 53 +++++++++++++++++++ .../templates/local-network.config.php.j2 | 4 ++ roles/nextcloud/templates/oidc.config.php.j2 | 6 +++ 5 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 roles/nextcloud/tasks/oidc.yml create mode 100644 roles/nextcloud/templates/local-network.config.php.j2 create mode 100644 roles/nextcloud/templates/oidc.config.php.j2 diff --git a/roles/nextcloud/defaults/main.yml b/roles/nextcloud/defaults/main.yml index 2e5a61e..1aa4ea3 100644 --- a/roles/nextcloud/defaults/main.yml +++ b/roles/nextcloud/defaults/main.yml @@ -14,6 +14,7 @@ nextcloud_image: "nextcloud:fpm" nextcloud_redis_image: "redis:latest" nextcloud_port: 80 nextcloud_extra_hosts: [] +nextcloud_allow_local_remote_servers: false # Set to true to allow requests to local network (dev only) nextcloud_postgres_image: "postgres:15" nextcloud_postgres_db: nextcloud @@ -55,4 +56,26 @@ nextcloud_apps_to_install: - spreed - user_ldap - user_oidc - - whiteboard \ No newline at end of file + - whiteboard + +# OIDC provider configuration +nextcloud_oidc_allow_selfsigned: false # Set to true to disable SSL verification for OIDC providers (dev only) +nextcloud_oidc_providers: [] +# - identifier: keycloak +# display_name: "Login with Keycloak" +# client_id: "nextcloud" +# client_secret: "changeme" +# discovery_url: "https://keycloak.example.com/realms/default/.well-known/openid-configuration" +# scope: "openid email profile" +# unique_uid: true +# check_bearer: false +# send_id_token_hint: true +# mapping: +# uid: preferred_username +# display_name: name +# email: email +# groups: groups + +# OIDC providers to remove +nextcloud_oidc_providers_removed: [] +# - old-provider \ No newline at end of file diff --git a/roles/nextcloud/tasks/main.yml b/roles/nextcloud/tasks/main.yml index f15103c..1d1a565 100644 --- a/roles/nextcloud/tasks/main.yml +++ b/roles/nextcloud/tasks/main.yml @@ -55,9 +55,21 @@ - (nextcloud_ready.stdout | from_json).installed == true changed_when: false +- name: Deploy local network config file + ansible.builtin.template: + src: local-network.config.php.j2 + dest: "{{ nextcloud_docker_volume_dir }}/nextcloud/config/local-network.config.php" + owner: www-data + group: www-data + mode: '0640' + - name: Install nextcloud plugins ansible.builtin.include_tasks: plugins.yml - name: Configure nextcloud collabora ansible.builtin.include_tasks: collabora.yml when: nextcloud_enable_collabora + +- name: Configure OIDC providers + ansible.builtin.include_tasks: oidc.yml + when: nextcloud_oidc_providers | length > 0 or nextcloud_oidc_providers_removed | length > 0 diff --git a/roles/nextcloud/tasks/oidc.yml b/roles/nextcloud/tasks/oidc.yml new file mode 100644 index 0000000..5a8d8f5 --- /dev/null +++ b/roles/nextcloud/tasks/oidc.yml @@ -0,0 +1,53 @@ +#SPDX-License-Identifier: MIT-0 +--- +# OIDC provider configuration for Nextcloud user_oidc app + +- name: Deploy OIDC config file + ansible.builtin.template: + src: oidc.config.php.j2 + dest: "{{ nextcloud_docker_volume_dir }}/nextcloud/config/oidc.config.php" + owner: www-data + group: www-data + mode: '0640' + +- name: Remove deleted OIDC providers + community.docker.docker_container_exec: + container: "{{ nextcloud_service_name }}-nextcloud-1" + command: php /var/www/html/occ user_oidc:provider:delete "{{ item }}" --force + loop: "{{ nextcloud_oidc_providers_removed }}" + register: oidc_delete_result + changed_when: "'deleted' in (oidc_delete_result.stdout | default('') | lower)" + failed_when: + - oidc_delete_result.rc != 0 + - "'not found' not in (oidc_delete_result.stderr | default('') | lower)" + - "'does not exist' not in (oidc_delete_result.stderr | default('') | lower)" + +- name: Create or update OIDC providers + vars: + _mapping: "{{ item.mapping | default({}) }}" + _base_args: + - php + - /var/www/html/occ + - user_oidc:provider + - "{{ item.identifier }}" + - "--clientid={{ item.client_id }}" + - "--clientsecret={{ item.client_secret }}" + - "--discoveryuri={{ item.discovery_url }}" + - "--unique-uid={{ '1' if item.unique_uid | default(true) else '0' }}" + - "--check-bearer={{ '1' if item.check_bearer | default(false) else '0' }}" + - "--send-id-token-hint={{ '1' if item.send_id_token_hint | default(true) else '0' }}" + _optional_args: "{{ + ((['--scope=' ~ item.scope]) if item.scope is defined else []) + + ((['--group-provisioning=1']) if item.group_provisioning | default(false) else []) + + ((['--mapping-uid=' ~ _mapping.uid]) if _mapping.uid is defined else []) + + ((['--mapping-display-name=' ~ _mapping.display_name]) if _mapping.display_name is defined else []) + + ((['--mapping-email=' ~ _mapping.email]) if _mapping.email is defined else []) + + ((['--mapping-groups=' ~ _mapping.groups]) if _mapping.groups is defined else []) + }}" + community.docker.docker_container_exec: + container: "{{ nextcloud_service_name }}-nextcloud-1" + argv: "{{ _base_args + _optional_args }}" + loop: "{{ nextcloud_oidc_providers }}" + register: oidc_create_result + changed_when: "'created' in (oidc_create_result.stdout | default('') | lower) or 'updated' in (oidc_create_result.stdout | default('') | lower)" + no_log: true \ No newline at end of file diff --git a/roles/nextcloud/templates/local-network.config.php.j2 b/roles/nextcloud/templates/local-network.config.php.j2 new file mode 100644 index 0000000..49f5b06 --- /dev/null +++ b/roles/nextcloud/templates/local-network.config.php.j2 @@ -0,0 +1,4 @@ + {{ nextcloud_allow_local_remote_servers | lower }}, +); \ No newline at end of file diff --git a/roles/nextcloud/templates/oidc.config.php.j2 b/roles/nextcloud/templates/oidc.config.php.j2 new file mode 100644 index 0000000..d09f638 --- /dev/null +++ b/roles/nextcloud/templates/oidc.config.php.j2 @@ -0,0 +1,6 @@ + array ( + 'httpclient.allowselfsigned' => {{ nextcloud_oidc_allow_selfsigned | lower }}, + ), +); From 6fad15e7ed2dac172b9e541906ebd9988e470e88 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 27 Feb 2026 13:44:43 +0100 Subject: [PATCH 03/39] chore: add empty boilerplate role for opencloud Signed-off-by: Bert-Jan Fikse --- roles/opencloud/README.md | 38 +++++++++++++++++++++++++++++++ roles/opencloud/defaults/main.yml | 3 +++ roles/opencloud/handlers/main.yml | 3 +++ roles/opencloud/meta/main.yml | 35 ++++++++++++++++++++++++++++ roles/opencloud/tasks/main.yml | 3 +++ roles/opencloud/tests/inventory | 3 +++ roles/opencloud/tests/test.yml | 6 +++++ roles/opencloud/vars/main.yml | 3 +++ 8 files changed, 94 insertions(+) create mode 100644 roles/opencloud/README.md create mode 100644 roles/opencloud/defaults/main.yml create mode 100644 roles/opencloud/handlers/main.yml create mode 100644 roles/opencloud/meta/main.yml create mode 100644 roles/opencloud/tasks/main.yml create mode 100644 roles/opencloud/tests/inventory create mode 100644 roles/opencloud/tests/test.yml create mode 100644 roles/opencloud/vars/main.yml diff --git a/roles/opencloud/README.md b/roles/opencloud/README.md new file mode 100644 index 0000000..225dd44 --- /dev/null +++ b/roles/opencloud/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/roles/opencloud/defaults/main.yml b/roles/opencloud/defaults/main.yml new file mode 100644 index 0000000..7a65c3e --- /dev/null +++ b/roles/opencloud/defaults/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# defaults file for opencloud diff --git a/roles/opencloud/handlers/main.yml b/roles/opencloud/handlers/main.yml new file mode 100644 index 0000000..335615c --- /dev/null +++ b/roles/opencloud/handlers/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# handlers file for opencloud diff --git a/roles/opencloud/meta/main.yml b/roles/opencloud/meta/main.yml new file mode 100644 index 0000000..6f91fd3 --- /dev/null +++ b/roles/opencloud/meta/main.yml @@ -0,0 +1,35 @@ +#SPDX-License-Identifier: MIT-0 +galaxy_info: + author: your name + description: your role description + company: your company (optional) + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.2 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/roles/opencloud/tasks/main.yml b/roles/opencloud/tasks/main.yml new file mode 100644 index 0000000..7d7408c --- /dev/null +++ b/roles/opencloud/tasks/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# tasks file for opencloud diff --git a/roles/opencloud/tests/inventory b/roles/opencloud/tests/inventory new file mode 100644 index 0000000..03ca42f --- /dev/null +++ b/roles/opencloud/tests/inventory @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +localhost + diff --git a/roles/opencloud/tests/test.yml b/roles/opencloud/tests/test.yml new file mode 100644 index 0000000..a139404 --- /dev/null +++ b/roles/opencloud/tests/test.yml @@ -0,0 +1,6 @@ +#SPDX-License-Identifier: MIT-0 +--- +- hosts: localhost + remote_user: root + roles: + - opencloud diff --git a/roles/opencloud/vars/main.yml b/roles/opencloud/vars/main.yml new file mode 100644 index 0000000..34f40a9 --- /dev/null +++ b/roles/opencloud/vars/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# vars file for opencloud From 59cd27a0312ed3e78f6edd5d8fc1d22025723507 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 27 Feb 2026 14:59:19 +0100 Subject: [PATCH 04/39] feat: add basic opencloud deployment Signed-off-by: Bert-Jan Fikse --- roles/opencloud/defaults/main.yml | 21 +++++++++ roles/opencloud/handlers/main.yml | 5 +++ roles/opencloud/tasks/main.yml | 30 +++++++++++++ .../opencloud/templates/docker-compose.yml.j2 | 44 +++++++++++++++++++ 4 files changed, 100 insertions(+) create mode 100644 roles/opencloud/templates/docker-compose.yml.j2 diff --git a/roles/opencloud/defaults/main.yml b/roles/opencloud/defaults/main.yml index 7a65c3e..67ef6a4 100644 --- a/roles/opencloud/defaults/main.yml +++ b/roles/opencloud/defaults/main.yml @@ -1,3 +1,24 @@ #SPDX-License-Identifier: MIT-0 --- # defaults file for opencloud + +# Base directory configuration (inherited from base role or defined here) +docker_compose_base_dir: /etc/docker/compose +docker_volume_base_dir: /srv/data + +# OpenCloud-specific configuration +opencloud_service_name: opencloud +opencloud_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ opencloud_service_name }}" +opencloud_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ opencloud_service_name }}" + +# Service configuration +opencloud_domain: "opencloud.local.test" +opencloud_image: "opencloudeu/opencloud:latest" +opencloud_port: 9200 +opencloud_admin_password: "admin" +opencloud_log_level: "warn" +opencloud_extra_hosts: [] + +# Traefik configuration +opencloud_traefik_network: "proxy" +opencloud_use_ssl: true \ No newline at end of file diff --git a/roles/opencloud/handlers/main.yml b/roles/opencloud/handlers/main.yml index 335615c..95b6986 100644 --- a/roles/opencloud/handlers/main.yml +++ b/roles/opencloud/handlers/main.yml @@ -1,3 +1,8 @@ #SPDX-License-Identifier: MIT-0 --- # handlers file for opencloud + +- name: restart opencloud + community.docker.docker_compose_v2: + project_src: "{{ opencloud_docker_compose_dir }}" + state: restarted \ No newline at end of file diff --git a/roles/opencloud/tasks/main.yml b/roles/opencloud/tasks/main.yml index 7d7408c..65b4c70 100644 --- a/roles/opencloud/tasks/main.yml +++ b/roles/opencloud/tasks/main.yml @@ -1,3 +1,33 @@ #SPDX-License-Identifier: MIT-0 --- # tasks file for opencloud + +- name: Create docker compose directory + file: + path: "{{ opencloud_docker_compose_dir }}" + state: directory + mode: '0755' + +- name: Create opencloud data directory + file: + path: "{{ opencloud_docker_volume_dir }}/data" + state: directory + mode: '0755' + +- name: Create opencloud config directory + file: + path: "{{ opencloud_docker_volume_dir }}/config" + state: directory + mode: '0755' + +- name: Create docker-compose file for opencloud + template: + src: docker-compose.yml.j2 + dest: "{{ opencloud_docker_compose_dir }}/docker-compose.yml" + mode: '0644' + notify: restart opencloud + +- name: Start opencloud container + community.docker.docker_compose_v2: + project_src: "{{ opencloud_docker_compose_dir }}" + state: present \ No newline at end of file diff --git a/roles/opencloud/templates/docker-compose.yml.j2 b/roles/opencloud/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..bc6d2c9 --- /dev/null +++ b/roles/opencloud/templates/docker-compose.yml.j2 @@ -0,0 +1,44 @@ +services: + opencloud: + image: {{ opencloud_image }} + container_name: {{ opencloud_service_name }} + restart: unless-stopped + entrypoint: + - /bin/sh + command: ["-c", "opencloud init || true; opencloud server"] + volumes: + - {{ opencloud_docker_volume_dir }}/config:/etc/ocis + - {{ opencloud_docker_volume_dir }}/data:/var/lib/ocis + environment: +{% if opencloud_use_ssl %} + OC_URL: "https://{{ opencloud_domain }}" +{% else %} + OC_URL: "http://{{ opencloud_domain }}" +{% endif %} + OC_INSECURE: "true" + OC_LOG_LEVEL: "{{ opencloud_log_level }}" + PROXY_TLS: "false" + IDM_ADMIN_PASSWORD: "{{ opencloud_admin_password }}" + networks: + - {{ opencloud_traefik_network }} +{% if opencloud_extra_hosts is defined and opencloud_extra_hosts | length > 0 %} + extra_hosts: +{% for host in opencloud_extra_hosts %} + - "{{ host }}" +{% endfor %} +{% endif %} + labels: + - traefik.enable=true + - traefik.docker.network={{ opencloud_traefik_network }} + - traefik.http.routers.{{ opencloud_service_name }}.rule=Host(`{{ opencloud_domain }}`) +{% if opencloud_use_ssl %} + - traefik.http.routers.{{ opencloud_service_name }}.entrypoints=websecure + - traefik.http.routers.{{ opencloud_service_name }}.tls=true +{% else %} + - traefik.http.routers.{{ opencloud_service_name }}.entrypoints=web +{% endif %} + - traefik.http.services.{{ opencloud_service_name }}.loadbalancer.server.port={{ opencloud_port }} + +networks: + {{ opencloud_traefik_network }}: + external: true \ No newline at end of file From 2dc9097707532ae67a31f4d4863d4d81e41728ac Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Thu, 5 Mar 2026 15:36:12 +0100 Subject: [PATCH 05/39] feat: add oidc provisioning for opencloud Signed-off-by: Bert-Jan Fikse --- roles/opencloud/defaults/main.yml | 15 ++++++++++++++- roles/opencloud/tasks/main.yml | 8 ++++++++ roles/opencloud/templates/csp-override.yaml.j2 | 13 +++++++++++++ roles/opencloud/templates/docker-compose.yml.j2 | 17 +++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 roles/opencloud/templates/csp-override.yaml.j2 diff --git a/roles/opencloud/defaults/main.yml b/roles/opencloud/defaults/main.yml index 67ef6a4..0de06e7 100644 --- a/roles/opencloud/defaults/main.yml +++ b/roles/opencloud/defaults/main.yml @@ -21,4 +21,17 @@ opencloud_extra_hosts: [] # Traefik configuration opencloud_traefik_network: "proxy" -opencloud_use_ssl: true \ No newline at end of file +opencloud_use_ssl: true + +# OIDC configuration (leave empty to use built-in IDP) +opencloud_oidc_issuer: "" +opencloud_oidc_client_id: "opencloud" +opencloud_oidc_client_secret: "" +opencloud_oidc_rewrite_wellknown: true +opencloud_oidc_user_claim: "preferred_username" +opencloud_oidc_user_cs3_claim: "username" +opencloud_oidc_account_edit_url: "" +opencloud_oidc_autoprovision_accounts: true + +# CSP configuration (extra URLs to allow in connect-src) +opencloud_csp_extra_connect_src: [] \ No newline at end of file diff --git a/roles/opencloud/tasks/main.yml b/roles/opencloud/tasks/main.yml index 65b4c70..b9f980f 100644 --- a/roles/opencloud/tasks/main.yml +++ b/roles/opencloud/tasks/main.yml @@ -20,6 +20,14 @@ state: directory mode: '0755' +- name: Create CSP override file + template: + src: csp-override.yaml.j2 + dest: "{{ opencloud_docker_volume_dir }}/config/csp-override.yaml" + mode: '0644' + when: opencloud_csp_extra_connect_src | length > 0 + notify: restart opencloud + - name: Create docker-compose file for opencloud template: src: docker-compose.yml.j2 diff --git a/roles/opencloud/templates/csp-override.yaml.j2 b/roles/opencloud/templates/csp-override.yaml.j2 new file mode 100644 index 0000000..f71cd9b --- /dev/null +++ b/roles/opencloud/templates/csp-override.yaml.j2 @@ -0,0 +1,13 @@ +directives: + connect-src: + - "'self'" + - "blob:" + - "https://raw.githubusercontent.com/opencloud-eu/awesome-apps/" + - "https://update.opencloud.eu/" +{% for url in opencloud_csp_extra_connect_src %} + - "{{ url }}" +{% endfor %} + script-src: + - "'self'" + - "'unsafe-inline'" + - "'unsafe-eval'" \ No newline at end of file diff --git a/roles/opencloud/templates/docker-compose.yml.j2 b/roles/opencloud/templates/docker-compose.yml.j2 index bc6d2c9..3785869 100644 --- a/roles/opencloud/templates/docker-compose.yml.j2 +++ b/roles/opencloud/templates/docker-compose.yml.j2 @@ -18,7 +18,24 @@ services: OC_INSECURE: "true" OC_LOG_LEVEL: "{{ opencloud_log_level }}" PROXY_TLS: "false" +{% if opencloud_csp_extra_connect_src | length > 0 %} + PROXY_CSP_CONFIG_FILE_OVERRIDE_LOCATION: "/etc/ocis/csp-override.yaml" +{% endif %} IDM_ADMIN_PASSWORD: "{{ opencloud_admin_password }}" +{% if opencloud_oidc_issuer %} + OC_OIDC_ISSUER: "{{ opencloud_oidc_issuer }}" + OC_OIDC_CLIENT_ID: "{{ opencloud_oidc_client_id }}" +{% if opencloud_oidc_client_secret %} + OC_OIDC_CLIENT_SECRET: "{{ opencloud_oidc_client_secret }}" +{% endif %} + PROXY_OIDC_REWRITE_WELLKNOWN: "{{ opencloud_oidc_rewrite_wellknown | string | lower }}" + PROXY_USER_OIDC_CLAIM: "{{ opencloud_oidc_user_claim }}" + PROXY_USER_CS3_CLAIM: "{{ opencloud_oidc_user_cs3_claim }}" + PROXY_AUTOPROVISION_ACCOUNTS: "{{ opencloud_oidc_autoprovision_accounts | string | lower }}" +{% if opencloud_oidc_account_edit_url %} + WEB_OPTION_ACCOUNT_EDIT_LINK_HREF: "{{ opencloud_oidc_account_edit_url }}" +{% endif %} +{% endif %} networks: - {{ opencloud_traefik_network }} {% if opencloud_extra_hosts is defined and opencloud_extra_hosts | length > 0 %} From fe85cc0f86495aad44d6e999b1f246b121b82afe Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Thu, 5 Mar 2026 16:24:12 +0100 Subject: [PATCH 06/39] feat: add s3 storage provisioning for opencloud Signed-off-by: Bert-Jan Fikse --- roles/opencloud/defaults/main.yml | 8 ++++++++ roles/opencloud/templates/docker-compose.yml.j2 | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/roles/opencloud/defaults/main.yml b/roles/opencloud/defaults/main.yml index 0de06e7..b1e6dcf 100644 --- a/roles/opencloud/defaults/main.yml +++ b/roles/opencloud/defaults/main.yml @@ -33,5 +33,13 @@ opencloud_oidc_user_cs3_claim: "username" opencloud_oidc_account_edit_url: "" opencloud_oidc_autoprovision_accounts: true +# S3 storage configuration (leave empty to use local storage) +opencloud_use_s3_storage: false +opencloud_s3_endpoint: "" +opencloud_s3_region: "us-east-1" +opencloud_s3_access_key: "" +opencloud_s3_secret_key: "" +opencloud_s3_bucket: "opencloud" + # CSP configuration (extra URLs to allow in connect-src) opencloud_csp_extra_connect_src: [] \ No newline at end of file diff --git a/roles/opencloud/templates/docker-compose.yml.j2 b/roles/opencloud/templates/docker-compose.yml.j2 index 3785869..7dff3b3 100644 --- a/roles/opencloud/templates/docker-compose.yml.j2 +++ b/roles/opencloud/templates/docker-compose.yml.j2 @@ -35,6 +35,14 @@ services: {% if opencloud_oidc_account_edit_url %} WEB_OPTION_ACCOUNT_EDIT_LINK_HREF: "{{ opencloud_oidc_account_edit_url }}" {% endif %} +{% endif %} +{% if opencloud_use_s3_storage %} + STORAGE_USERS_DRIVER: "decomposeds3" + STORAGE_USERS_DECOMPOSEDS3_ENDPOINT: "{{ opencloud_s3_endpoint }}" + STORAGE_USERS_DECOMPOSEDS3_REGION: "{{ opencloud_s3_region }}" + STORAGE_USERS_DECOMPOSEDS3_ACCESS_KEY: "{{ opencloud_s3_access_key }}" + STORAGE_USERS_DECOMPOSEDS3_SECRET_KEY: "{{ opencloud_s3_secret_key }}" + STORAGE_USERS_DECOMPOSEDS3_BUCKET: "{{ opencloud_s3_bucket }}" {% endif %} networks: - {{ opencloud_traefik_network }} From 064b939d0626705e99ec09891ce738a2fd1c4f4e Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Thu, 5 Mar 2026 16:34:50 +0100 Subject: [PATCH 07/39] chore: add empty role boilerplate for collabora Signed-off-by: Bert-Jan Fikse --- roles/collabora/README.md | 38 +++++++++++++++++++++++++++++++ roles/collabora/defaults/main.yml | 3 +++ roles/collabora/handlers/main.yml | 3 +++ roles/collabora/meta/main.yml | 35 ++++++++++++++++++++++++++++ roles/collabora/tasks/main.yml | 3 +++ roles/collabora/tests/inventory | 3 +++ roles/collabora/tests/test.yml | 6 +++++ roles/collabora/vars/main.yml | 3 +++ 8 files changed, 94 insertions(+) create mode 100644 roles/collabora/README.md create mode 100644 roles/collabora/defaults/main.yml create mode 100644 roles/collabora/handlers/main.yml create mode 100644 roles/collabora/meta/main.yml create mode 100644 roles/collabora/tasks/main.yml create mode 100644 roles/collabora/tests/inventory create mode 100644 roles/collabora/tests/test.yml create mode 100644 roles/collabora/vars/main.yml diff --git a/roles/collabora/README.md b/roles/collabora/README.md new file mode 100644 index 0000000..225dd44 --- /dev/null +++ b/roles/collabora/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/roles/collabora/defaults/main.yml b/roles/collabora/defaults/main.yml new file mode 100644 index 0000000..0f490f3 --- /dev/null +++ b/roles/collabora/defaults/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# defaults file for collabora diff --git a/roles/collabora/handlers/main.yml b/roles/collabora/handlers/main.yml new file mode 100644 index 0000000..47ac933 --- /dev/null +++ b/roles/collabora/handlers/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# handlers file for collabora diff --git a/roles/collabora/meta/main.yml b/roles/collabora/meta/main.yml new file mode 100644 index 0000000..6f91fd3 --- /dev/null +++ b/roles/collabora/meta/main.yml @@ -0,0 +1,35 @@ +#SPDX-License-Identifier: MIT-0 +galaxy_info: + author: your name + description: your role description + company: your company (optional) + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.2 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/roles/collabora/tasks/main.yml b/roles/collabora/tasks/main.yml new file mode 100644 index 0000000..ec3bb73 --- /dev/null +++ b/roles/collabora/tasks/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# tasks file for collabora diff --git a/roles/collabora/tests/inventory b/roles/collabora/tests/inventory new file mode 100644 index 0000000..03ca42f --- /dev/null +++ b/roles/collabora/tests/inventory @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +localhost + diff --git a/roles/collabora/tests/test.yml b/roles/collabora/tests/test.yml new file mode 100644 index 0000000..80a6a59 --- /dev/null +++ b/roles/collabora/tests/test.yml @@ -0,0 +1,6 @@ +#SPDX-License-Identifier: MIT-0 +--- +- hosts: localhost + remote_user: root + roles: + - collabora diff --git a/roles/collabora/vars/main.yml b/roles/collabora/vars/main.yml new file mode 100644 index 0000000..5787a3b --- /dev/null +++ b/roles/collabora/vars/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# vars file for collabora From d3d7bb9ba5d282898352a54e6917e8d6f1446bb4 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Thu, 5 Mar 2026 17:09:06 +0100 Subject: [PATCH 08/39] chore: add central collabora service instead of providing one for owncloud and nextcloud separately Signed-off-by: Bert-Jan Fikse --- roles/collabora/defaults/main.yml | 25 +++++++++++++ roles/collabora/handlers/main.yml | 5 +++ roles/collabora/tasks/main.yml | 18 ++++++++++ .../collabora/templates/docker-compose.yml.j2 | 36 +++++++++++++++++++ roles/nextcloud/defaults/main.yml | 2 -- .../nextcloud/templates/docker-compose.yml.j2 | 28 --------------- 6 files changed, 84 insertions(+), 30 deletions(-) create mode 100644 roles/collabora/templates/docker-compose.yml.j2 diff --git a/roles/collabora/defaults/main.yml b/roles/collabora/defaults/main.yml index 0f490f3..f5b48c8 100644 --- a/roles/collabora/defaults/main.yml +++ b/roles/collabora/defaults/main.yml @@ -1,3 +1,28 @@ #SPDX-License-Identifier: MIT-0 --- # defaults file for collabora + +# Base directory configuration (inherited from base role or defined here) +docker_compose_base_dir: /etc/docker/compose +docker_volume_base_dir: /srv/data + +# Collabora-specific configuration +collabora_service_name: collabora +collabora_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ collabora_service_name }}" + +# Service configuration +collabora_domain: "office.local.test" +collabora_image: "collabora/code:latest" +collabora_port: 9980 +collabora_extra_hosts: [] + +# Traefik configuration +collabora_traefik_network: "proxy" +collabora_use_ssl: true + +# Allowed WOPI host domains (Nextcloud, OpenCloud, etc.) +# These domains are allowed to open documents via Collabora. +# Each entry is used as a regex pattern (dots are auto-escaped). +collabora_allowed_domains: + - "nextcloud.local.test" + - "opencloud.local.test" \ No newline at end of file diff --git a/roles/collabora/handlers/main.yml b/roles/collabora/handlers/main.yml index 47ac933..bfd2b02 100644 --- a/roles/collabora/handlers/main.yml +++ b/roles/collabora/handlers/main.yml @@ -1,3 +1,8 @@ #SPDX-License-Identifier: MIT-0 --- # handlers file for collabora + +- name: restart collabora + community.docker.docker_compose_v2: + project_src: "{{ collabora_docker_compose_dir }}" + state: restarted \ No newline at end of file diff --git a/roles/collabora/tasks/main.yml b/roles/collabora/tasks/main.yml index ec3bb73..1893498 100644 --- a/roles/collabora/tasks/main.yml +++ b/roles/collabora/tasks/main.yml @@ -1,3 +1,21 @@ #SPDX-License-Identifier: MIT-0 --- # tasks file for collabora + +- name: Create docker compose directory + file: + path: "{{ collabora_docker_compose_dir }}" + state: directory + mode: '0755' + +- name: Create docker-compose file for collabora + template: + src: docker-compose.yml.j2 + dest: "{{ collabora_docker_compose_dir }}/docker-compose.yml" + mode: '0644' + notify: restart collabora + +- name: Start collabora container + community.docker.docker_compose_v2: + project_src: "{{ collabora_docker_compose_dir }}" + state: present \ No newline at end of file diff --git a/roles/collabora/templates/docker-compose.yml.j2 b/roles/collabora/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..cb284fb --- /dev/null +++ b/roles/collabora/templates/docker-compose.yml.j2 @@ -0,0 +1,36 @@ +services: + collabora: + image: {{ collabora_image }} + container_name: {{ collabora_service_name }} + restart: unless-stopped + environment: + domain: {{ collabora_allowed_domains | map('replace', '.', '\\.') | map('regex_replace', '^(.*)$', '^\\1$$') | join('|') }} + extra_params: >- + --o:ssl.enable=false + --o:ssl.termination=true + --o:net.frame_ancestors={{ collabora_allowed_domains | map('regex_replace', '^(.*)$', 'https://\\1') | join(' ') }} + cap_add: + - MKNOD + networks: + - {{ collabora_traefik_network }} +{% if collabora_extra_hosts is defined and collabora_extra_hosts | length > 0 %} + extra_hosts: +{% for host in collabora_extra_hosts %} + - "{{ host }}" +{% endfor %} +{% endif %} + labels: + - traefik.enable=true + - traefik.docker.network={{ collabora_traefik_network }} + - traefik.http.routers.{{ collabora_service_name }}.rule=Host(`{{ collabora_domain }}`) + - traefik.http.services.{{ collabora_service_name }}.loadbalancer.server.port={{ collabora_port }} +{% if collabora_use_ssl %} + - traefik.http.routers.{{ collabora_service_name }}.entrypoints=websecure + - traefik.http.routers.{{ collabora_service_name }}.tls=true +{% else %} + - traefik.http.routers.{{ collabora_service_name }}.entrypoints=web +{% endif %} + +networks: + {{ collabora_traefik_network }}: + external: true \ No newline at end of file diff --git a/roles/nextcloud/defaults/main.yml b/roles/nextcloud/defaults/main.yml index 1aa4ea3..7110ca5 100644 --- a/roles/nextcloud/defaults/main.yml +++ b/roles/nextcloud/defaults/main.yml @@ -27,8 +27,6 @@ nextcloud_use_ssl: true nextcloud_enable_collabora: true nextcloud_collabora_domain: "office.local.test" -nextcloud_collabora_service_name: collabora -nextcloud_collabora_image: collabora/code:latest nextcloud_collabora_disable_cert_verification: false nextcloud_use_s3_storage: false diff --git a/roles/nextcloud/templates/docker-compose.yml.j2 b/roles/nextcloud/templates/docker-compose.yml.j2 index b8a8a4d..3ae9fd5 100644 --- a/roles/nextcloud/templates/docker-compose.yml.j2 +++ b/roles/nextcloud/templates/docker-compose.yml.j2 @@ -109,34 +109,6 @@ services: {% endfor %} {% endif %} -{% if nextcloud_enable_collabora %} - collabora: - image: {{ nextcloud_collabora_image }} - restart: always - environment: - domain: ^{{ nextcloud_domain | replace('.', '\\.') }}$ - extra_params: >- - --o:ssl.enable=false - --o:ssl.termination=true - --o:net.frame_ancestors=https://{{ nextcloud_domain }} - cap_add: - - MKNOD - networks: - - {{ nextcloud_traefik_network }} - labels: - - traefik.enable=true - - traefik.docker.network={{ nextcloud_traefik_network }} - - traefik.http.routers.{{ nextcloud_collabora_service_name }}.rule=Host(`{{ nextcloud_collabora_domain }}`) - - traefik.http.services.{{ nextcloud_collabora_service_name }}.loadbalancer.server.port=9980 -{% if nextcloud_use_ssl %} - - traefik.http.routers.{{ nextcloud_collabora_service_name }}.entrypoints=websecure - - traefik.http.routers.{{ nextcloud_collabora_service_name }}.tls=true -{% else %} - - traefik.http.routers.{{ nextcloud_collabora_service_name }}.entrypoints=web -{% endif %} - -{% endif %} - networks: {{ nextcloud_backend_network }}: {{ nextcloud_traefik_network }}: From 6be4a50f8fc3196dca64bc7878a891402f8e61a7 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 6 Mar 2026 17:00:33 +0100 Subject: [PATCH 09/39] chore: ensure we can use the same collabora instance for multiple cloud instances Signed-off-by: Bert-Jan Fikse --- roles/collabora/defaults/main.yml | 13 +- roles/collabora/tasks/main.yml | 13 + roles/collabora/templates/coolwsd.xml.j2 | 340 ++++++++++++++++++ .../collabora/templates/docker-compose.yml.j2 | 8 +- roles/nextcloud/tasks/collabora.yml | 7 +- roles/opencloud/defaults/main.yml | 5 + .../opencloud/templates/docker-compose.yml.j2 | 31 +- 7 files changed, 405 insertions(+), 12 deletions(-) create mode 100644 roles/collabora/templates/coolwsd.xml.j2 diff --git a/roles/collabora/defaults/main.yml b/roles/collabora/defaults/main.yml index f5b48c8..3cfb559 100644 --- a/roles/collabora/defaults/main.yml +++ b/roles/collabora/defaults/main.yml @@ -9,6 +9,7 @@ docker_volume_base_dir: /srv/data # Collabora-specific configuration collabora_service_name: collabora collabora_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ collabora_service_name }}" +collabora_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ collabora_service_name }}" # Service configuration collabora_domain: "office.local.test" @@ -20,9 +21,15 @@ collabora_extra_hosts: [] collabora_traefik_network: "proxy" collabora_use_ssl: true -# Allowed WOPI host domains (Nextcloud, OpenCloud, etc.) -# These domains are allowed to open documents via Collabora. +# SSL verification for WOPI callbacks (set to false for self-signed certs) +collabora_ssl_verification: true + +# Allowed WOPI host domains (Nextcloud, OpenCloud WOPI server, etc.) +# These domains are allowed to send WOPI requests to Collabora. # Each entry is used as a regex pattern (dots are auto-escaped). collabora_allowed_domains: - "nextcloud.local.test" - - "opencloud.local.test" \ No newline at end of file + +# Domains allowed to embed Collabora in an iframe (Nextcloud, OpenCloud, etc.) +collabora_frame_ancestors: + - "nextcloud.local.test" \ No newline at end of file diff --git a/roles/collabora/tasks/main.yml b/roles/collabora/tasks/main.yml index 1893498..b6146c7 100644 --- a/roles/collabora/tasks/main.yml +++ b/roles/collabora/tasks/main.yml @@ -8,6 +8,19 @@ state: directory mode: '0755' +- name: Create collabora volume directory + file: + path: "{{ collabora_docker_volume_dir }}" + state: directory + mode: '0755' + +- name: Create coolwsd configuration + template: + src: coolwsd.xml.j2 + dest: "{{ collabora_docker_volume_dir }}/coolwsd.xml" + mode: '0644' + notify: restart collabora + - name: Create docker-compose file for collabora template: src: docker-compose.yml.j2 diff --git a/roles/collabora/templates/coolwsd.xml.j2 b/roles/collabora/templates/coolwsd.xml.j2 new file mode 100644 index 0000000..df5dd50 --- /dev/null +++ b/roles/collabora/templates/coolwsd.xml.j2 @@ -0,0 +1,340 @@ + + + + + + false + + + de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru + + + false + + + + true + + + + + false + + + + + + + true + + + + false + true + + + 4 + 10 + true + + + 4 + 5 + 5 + 120 + false + 96 + 3600 + 30 + 300 + true + true + false + 0 + 8000 + 0 + 0 + 100 + 5 + 100 + 500 + 5000 + + 10000 + 60 + 300 + 3072 + 85 + 120 + + + + + 300 + 900 + + 6 + + + + + + true + warning + trace + Socket,WebSocket,Admin,Pixel + notice + fatal + false + -INFO-WARN + + /var/log/coolwsd.log + never + timestamp + true + 10 days + 10 + true + false + + + false + 82589933 + + false + false + false + + + true + + + true + true + + /var/log/coolwsd-ui-cmd.log + 10 + true + false + + + + + /var/log/coolwsd.trace.json + + + false + + + + + + + + false + + + + + all + any + + + 192\.168\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:192\.168\.[0-9]{1,3}\.[0-9]{1,3} + 127\.0\.0\.1 + ::ffff:127\.0\.0\.1 + ::1 + 172\.1[6789]\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:172\.1[6789]\.[0-9]{1,3}\.[0-9]{1,3} + 172\.2[0-9]\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:172\.2[0-9]\.[0-9]{1,3}\.[0-9]{1,3} + 172\.3[01]\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:172\.3[01]\.[0-9]{1,3}\.[0-9]{1,3} + 10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} + + + 192\.168\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:192\.168\.[0-9]{1,3}\.[0-9]{1,3} + 127\.0\.0\.1 + ::ffff:127\.0\.0\.1 + ::1 + 172\.1[6789]\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:172\.1[6789]\.[0-9]{1,3}\.[0-9]{1,3} + 172\.2[0-9]\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:172\.2[0-9]\.[0-9]{1,3}\.[0-9]{1,3} + 172\.3[01]\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:172\.3[01]\.[0-9]{1,3}\.[0-9]{1,3} + 10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} + ::ffff:10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} + localhost + + + {{ collabora_frame_ancestors | map('regex_replace', '^(.*)$', 'https://\\1') | join(' ') }} + 30 + false + + + + true + false + /etc/coolwsd/cert.pem + /etc/coolwsd/key.pem + /etc/coolwsd/ca-chain.cert.pem + false + + + 1000 + + + + + false + 31536000 + + + + + true + true + 1800 + false + 1 + false + false + false + + + + + + + + 0.2 + + + + + default + true + true + + + + + + 0 + + 900 + + + +{% for domain in collabora_allowed_domains %} + + https://{{ domain }}:443 + +{% endfor %} + + + false + + + true + + + + + + + + + + true + false + + + + true + true + true + true + + + + + + + 250 + 5 + + 3000 + + + + + 1000 + + + + false + false + false + false + false + false + + + + 3600 + + + + + + + false + + + + + + + log + + + + + 180 + + false + + + + + + + + false + + + + true + + + https://help.collaboraoffice.com/help.html? + + + false + + + + false + false + + + + true + + + \ No newline at end of file diff --git a/roles/collabora/templates/docker-compose.yml.j2 b/roles/collabora/templates/docker-compose.yml.j2 index cb284fb..c0f589e 100644 --- a/roles/collabora/templates/docker-compose.yml.j2 +++ b/roles/collabora/templates/docker-compose.yml.j2 @@ -4,11 +4,9 @@ services: container_name: {{ collabora_service_name }} restart: unless-stopped environment: - domain: {{ collabora_allowed_domains | map('replace', '.', '\\.') | map('regex_replace', '^(.*)$', '^\\1$$') | join('|') }} - extra_params: >- - --o:ssl.enable=false - --o:ssl.termination=true - --o:net.frame_ancestors={{ collabora_allowed_domains | map('regex_replace', '^(.*)$', 'https://\\1') | join(' ') }} + extra_params: "--o:ssl.enable=false --o:ssl.termination=true --o:ssl.ssl_verification={{ collabora_ssl_verification | string | lower }}" + volumes: + - {{ collabora_docker_volume_dir }}/coolwsd.xml:/etc/coolwsd/coolwsd.xml:ro cap_add: - MKNOD networks: diff --git a/roles/nextcloud/tasks/collabora.yml b/roles/nextcloud/tasks/collabora.yml index a165ffa..05c56e4 100644 --- a/roles/nextcloud/tasks/collabora.yml +++ b/roles/nextcloud/tasks/collabora.yml @@ -14,4 +14,9 @@ - name: Set Collabora WOPI allowlist community.docker.docker_container_exec: container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1" - command: php /var/www/html/occ config:app:set richdocuments wopi_allowlist --value='' \ No newline at end of file + command: php /var/www/html/occ config:app:set richdocuments wopi_allowlist --value='' + +- name: Activate richdocuments configuration (fetch discovery from Collabora) + community.docker.docker_container_exec: + container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1" + command: php /var/www/html/occ richdocuments:activate-config \ No newline at end of file diff --git a/roles/opencloud/defaults/main.yml b/roles/opencloud/defaults/main.yml index b1e6dcf..9e43dcc 100644 --- a/roles/opencloud/defaults/main.yml +++ b/roles/opencloud/defaults/main.yml @@ -41,5 +41,10 @@ opencloud_s3_access_key: "" opencloud_s3_secret_key: "" opencloud_s3_bucket: "opencloud" +# Collabora integration (set opencloud_collabora_domain to enable) +opencloud_collabora_domain: "" +opencloud_wopi_domain: "" +opencloud_collabora_insecure: true + # CSP configuration (extra URLs to allow in connect-src) opencloud_csp_extra_connect_src: [] \ No newline at end of file diff --git a/roles/opencloud/templates/docker-compose.yml.j2 b/roles/opencloud/templates/docker-compose.yml.j2 index 7dff3b3..bc142a2 100644 --- a/roles/opencloud/templates/docker-compose.yml.j2 +++ b/roles/opencloud/templates/docker-compose.yml.j2 @@ -7,8 +7,8 @@ services: - /bin/sh command: ["-c", "opencloud init || true; opencloud server"] volumes: - - {{ opencloud_docker_volume_dir }}/config:/etc/ocis - - {{ opencloud_docker_volume_dir }}/data:/var/lib/ocis + - {{ opencloud_docker_volume_dir }}/config:/etc/opencloud + - {{ opencloud_docker_volume_dir }}/data:/var/lib/opencloud environment: {% if opencloud_use_ssl %} OC_URL: "https://{{ opencloud_domain }}" @@ -19,7 +19,7 @@ services: OC_LOG_LEVEL: "{{ opencloud_log_level }}" PROXY_TLS: "false" {% if opencloud_csp_extra_connect_src | length > 0 %} - PROXY_CSP_CONFIG_FILE_OVERRIDE_LOCATION: "/etc/ocis/csp-override.yaml" + PROXY_CSP_CONFIG_FILE_OVERRIDE_LOCATION: "/etc/opencloud/csp-override.yaml" {% endif %} IDM_ADMIN_PASSWORD: "{{ opencloud_admin_password }}" {% if opencloud_oidc_issuer %} @@ -43,6 +43,19 @@ services: STORAGE_USERS_DECOMPOSEDS3_ACCESS_KEY: "{{ opencloud_s3_access_key }}" STORAGE_USERS_DECOMPOSEDS3_SECRET_KEY: "{{ opencloud_s3_secret_key }}" STORAGE_USERS_DECOMPOSEDS3_BUCKET: "{{ opencloud_s3_bucket }}" +{% endif %} +{% if opencloud_collabora_domain %} + OC_ADD_RUN_SERVICES: "collaboration" + COLLABORA_DOMAIN: "{{ opencloud_collabora_domain }}" + COLLABORATION_APP_NAME: "CollaboraOnline" + COLLABORATION_APP_PRODUCT: "Collabora" + COLLABORATION_APP_ADDR: "https://{{ opencloud_collabora_domain }}" + COLLABORATION_APP_INSECURE: "{{ opencloud_collabora_insecure | string | lower }}" + COLLABORATION_APP_PROOF_DISABLE: "{{ opencloud_collabora_insecure | string | lower }}" + COLLABORATION_CS3API_DATAGATEWAY_INSECURE: "{{ opencloud_collabora_insecure | string | lower }}" + COLLABORATION_HTTP_ADDR: "0.0.0.0:9300" + COLLABORATION_WOPI_SRC: "https://{{ opencloud_wopi_domain }}" + FRONTEND_APP_HANDLER_SECURE_VIEW_APP_ADDR: "eu.opencloud.api.collaboration" {% endif %} networks: - {{ opencloud_traefik_network }} @@ -63,6 +76,18 @@ services: - traefik.http.routers.{{ opencloud_service_name }}.entrypoints=web {% endif %} - traefik.http.services.{{ opencloud_service_name }}.loadbalancer.server.port={{ opencloud_port }} +{% if opencloud_collabora_domain %} + - traefik.http.routers.{{ opencloud_service_name }}.service={{ opencloud_service_name }} + - traefik.http.routers.{{ opencloud_service_name }}-wopi.rule=Host(`{{ opencloud_wopi_domain }}`) + - traefik.http.routers.{{ opencloud_service_name }}-wopi.service={{ opencloud_service_name }}-wopi + - traefik.http.services.{{ opencloud_service_name }}-wopi.loadbalancer.server.port=9300 +{% if opencloud_use_ssl %} + - traefik.http.routers.{{ opencloud_service_name }}-wopi.entrypoints=websecure + - traefik.http.routers.{{ opencloud_service_name }}-wopi.tls=true +{% else %} + - traefik.http.routers.{{ opencloud_service_name }}-wopi.entrypoints=web +{% endif %} +{% endif %} networks: {{ opencloud_traefik_network }}: From 244e378d9df4bb9b28f47130686752189c1540fc Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 6 Mar 2026 17:18:01 +0100 Subject: [PATCH 10/39] fix: use correct file ownership for nextcloud volumes Signed-off-by: Bert-Jan Fikse --- roles/opencloud/tasks/main.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/roles/opencloud/tasks/main.yml b/roles/opencloud/tasks/main.yml index b9f980f..d3ce5ba 100644 --- a/roles/opencloud/tasks/main.yml +++ b/roles/opencloud/tasks/main.yml @@ -12,18 +12,24 @@ file: path: "{{ opencloud_docker_volume_dir }}/data" state: directory - mode: '0755' + owner: "1000" + group: "1000" + mode: '0750' - name: Create opencloud config directory file: path: "{{ opencloud_docker_volume_dir }}/config" state: directory - mode: '0755' + owner: "1000" + group: "1000" + mode: '0750' - name: Create CSP override file template: src: csp-override.yaml.j2 dest: "{{ opencloud_docker_volume_dir }}/config/csp-override.yaml" + owner: "1000" + group: "1000" mode: '0644' when: opencloud_csp_extra_connect_src | length > 0 notify: restart opencloud From dae32362edb6458ef955715c517c839dc0f74adc Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 6 Mar 2026 17:36:40 +0100 Subject: [PATCH 11/39] chore: add empty boilerplate role for 389ds Signed-off-by: Bert-Jan Fikse --- roles/389ds/README.md | 38 +++++++++++++++++++++++++++++++++++ roles/389ds/defaults/main.yml | 3 +++ roles/389ds/handlers/main.yml | 3 +++ roles/389ds/meta/main.yml | 35 ++++++++++++++++++++++++++++++++ roles/389ds/tasks/main.yml | 3 +++ roles/389ds/tests/inventory | 3 +++ roles/389ds/tests/test.yml | 6 ++++++ roles/389ds/vars/main.yml | 3 +++ 8 files changed, 94 insertions(+) create mode 100644 roles/389ds/README.md create mode 100644 roles/389ds/defaults/main.yml create mode 100644 roles/389ds/handlers/main.yml create mode 100644 roles/389ds/meta/main.yml create mode 100644 roles/389ds/tasks/main.yml create mode 100644 roles/389ds/tests/inventory create mode 100644 roles/389ds/tests/test.yml create mode 100644 roles/389ds/vars/main.yml diff --git a/roles/389ds/README.md b/roles/389ds/README.md new file mode 100644 index 0000000..225dd44 --- /dev/null +++ b/roles/389ds/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/roles/389ds/defaults/main.yml b/roles/389ds/defaults/main.yml new file mode 100644 index 0000000..0d9ab7d --- /dev/null +++ b/roles/389ds/defaults/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# defaults file for 389ds diff --git a/roles/389ds/handlers/main.yml b/roles/389ds/handlers/main.yml new file mode 100644 index 0000000..9201934 --- /dev/null +++ b/roles/389ds/handlers/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# handlers file for 389ds diff --git a/roles/389ds/meta/main.yml b/roles/389ds/meta/main.yml new file mode 100644 index 0000000..6f91fd3 --- /dev/null +++ b/roles/389ds/meta/main.yml @@ -0,0 +1,35 @@ +#SPDX-License-Identifier: MIT-0 +galaxy_info: + author: your name + description: your role description + company: your company (optional) + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.2 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/roles/389ds/tasks/main.yml b/roles/389ds/tasks/main.yml new file mode 100644 index 0000000..18a1e4e --- /dev/null +++ b/roles/389ds/tasks/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# tasks file for 389ds diff --git a/roles/389ds/tests/inventory b/roles/389ds/tests/inventory new file mode 100644 index 0000000..03ca42f --- /dev/null +++ b/roles/389ds/tests/inventory @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +localhost + diff --git a/roles/389ds/tests/test.yml b/roles/389ds/tests/test.yml new file mode 100644 index 0000000..d7b9ef6 --- /dev/null +++ b/roles/389ds/tests/test.yml @@ -0,0 +1,6 @@ +#SPDX-License-Identifier: MIT-0 +--- +- hosts: localhost + remote_user: root + roles: + - 389ds diff --git a/roles/389ds/vars/main.yml b/roles/389ds/vars/main.yml new file mode 100644 index 0000000..02d1889 --- /dev/null +++ b/roles/389ds/vars/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# vars file for 389ds From 700cafed0ee355b5930c43b49980ec2eb00e519e Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 6 Mar 2026 17:54:07 +0100 Subject: [PATCH 12/39] feat: add basic ds389 docker setup and configuration Signed-off-by: Bert-Jan Fikse --- roles/389ds/defaults/main.yml | 23 ++++++++++++++++ roles/389ds/tasks/main.yml | 29 +++++++++++++++++++++ roles/389ds/templates/docker-compose.yml.j2 | 18 +++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 roles/389ds/templates/docker-compose.yml.j2 diff --git a/roles/389ds/defaults/main.yml b/roles/389ds/defaults/main.yml index 0d9ab7d..00bf5d1 100644 --- a/roles/389ds/defaults/main.yml +++ b/roles/389ds/defaults/main.yml @@ -1,3 +1,26 @@ #SPDX-License-Identifier: MIT-0 --- # defaults file for 389ds + +# Base directory configuration (inherited from base role or defined here) +docker_compose_base_dir: /etc/docker/compose +docker_volume_base_dir: /srv/data + +# 389ds-specific configuration +ds389_service_name: 389ds +ds389_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ ds389_service_name }}" +ds389_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ ds389_service_name }}" + +# 389ds service configuration +ds389_image: "docker.io/389ds/dirsrv:3.1" +ds389_suffix: "dc=example,dc=com" +ds389_root_dn: "cn=Directory Manager" +ds389_root_password: "changeme" + +# Instance configuration +ds389_instance_name: "localhost" + +# Network configuration +ds389_backend_network: "backend" +ds389_ldap_port: 3389 +ds389_ldaps_port: 3636 \ No newline at end of file diff --git a/roles/389ds/tasks/main.yml b/roles/389ds/tasks/main.yml index 18a1e4e..277d2dd 100644 --- a/roles/389ds/tasks/main.yml +++ b/roles/389ds/tasks/main.yml @@ -1,3 +1,32 @@ #SPDX-License-Identifier: MIT-0 --- # tasks file for 389ds + +- name: Create docker compose directory + file: + path: "{{ ds389_docker_compose_dir }}" + state: directory + mode: '0755' + +- name: Create 389ds data directory + file: + path: "{{ ds389_docker_volume_dir }}/data" + state: directory + mode: '0755' + +- name: Create 389ds config directory + file: + path: "{{ ds389_docker_volume_dir }}/config" + state: directory + mode: '0755' + +- name: Create docker-compose file for 389ds + template: + src: docker-compose.yml.j2 + dest: "{{ ds389_docker_compose_dir }}/docker-compose.yml" + mode: '0644' + +- name: Start 389ds container + community.docker.docker_compose_v2: + project_src: "{{ ds389_docker_compose_dir }}" + state: present \ No newline at end of file diff --git a/roles/389ds/templates/docker-compose.yml.j2 b/roles/389ds/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..a8842e7 --- /dev/null +++ b/roles/389ds/templates/docker-compose.yml.j2 @@ -0,0 +1,18 @@ +services: + {{ ds389_service_name }}: + image: {{ ds389_image }} + restart: unless-stopped + environment: + DS_SUFFIX_NAME: {{ ds389_suffix }} + DS_DM_PASSWORD: {{ ds389_root_password }} + ports: + - "{{ ds389_ldap_port }}:3389" + - "{{ ds389_ldaps_port }}:3636" + volumes: + - {{ ds389_docker_volume_dir }}/data:/data + - {{ ds389_docker_volume_dir }}/config:/etc/dirsrv/slapd-{{ ds389_instance_name }} + networks: + - {{ ds389_backend_network }} + +networks: + {{ ds389_backend_network }}: From 59d017490595431577411914fb7b276b61fd81b2 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 13 Mar 2026 10:46:49 +0100 Subject: [PATCH 13/39] feat: add ldap provisioning to nextcloud Signed-off-by: Bert-Jan Fikse --- roles/nextcloud/defaults/main.yml | 23 ++++++++++++++++- roles/nextcloud/tasks/ldap.yml | 41 +++++++++++++++++++++++++++++++ roles/nextcloud/tasks/main.yml | 4 +++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 roles/nextcloud/tasks/ldap.yml diff --git a/roles/nextcloud/defaults/main.yml b/roles/nextcloud/defaults/main.yml index 7110ca5..e40ea55 100644 --- a/roles/nextcloud/defaults/main.yml +++ b/roles/nextcloud/defaults/main.yml @@ -76,4 +76,25 @@ nextcloud_oidc_providers: [] # OIDC providers to remove nextcloud_oidc_providers_removed: [] -# - old-provider \ No newline at end of file +# - old-provider + +# LDAP configuration +nextcloud_ldap_enabled: false +nextcloud_ldap_config: {} +# Example for 389ds with Keycloak user federation: +# ldapHost: "ldaps://389ds" +# ldapPort: "3636" +# ldapAgentName: "cn=Directory Manager" +# ldapAgentPassword: "changeme" +# ldapBase: "dc=example,dc=com" +# ldapBaseUsers: "ou=users,dc=example,dc=com" +# ldapBaseGroups: "dc=example,dc=com" +# ldapTLS: "0" +# turnOffCertCheck: "0" +# ldapUserFilter: "(&(objectclass=inetOrgPerson)(uid=*))" +# ldapLoginFilter: "(&(objectclass=inetOrgPerson)(uid=%uid))" +# ldapUserDisplayName: "displayname" +# ldapEmailAttribute: "mail" +# ldapExpertUsernameAttr: "uid" +# ldapExpertUUIDUserAttr: "nsuniqueid" +# ldapConfigurationActive: "1" \ No newline at end of file diff --git a/roles/nextcloud/tasks/ldap.yml b/roles/nextcloud/tasks/ldap.yml new file mode 100644 index 0000000..dcb2392 --- /dev/null +++ b/roles/nextcloud/tasks/ldap.yml @@ -0,0 +1,41 @@ +#SPDX-License-Identifier: MIT-0 +--- +# LDAP configuration for Nextcloud user_ldap app + +- name: Check if LDAP configuration exists + community.docker.docker_container_exec: + container: "{{ nextcloud_service_name }}-nextcloud-1" + command: php /var/www/html/occ ldap:show-config + register: ldap_show_config + changed_when: false + +- name: Create LDAP configuration + community.docker.docker_container_exec: + container: "{{ nextcloud_service_name }}-nextcloud-1" + command: php /var/www/html/occ ldap:create-empty-config + when: "'s01' not in ldap_show_config.stdout" + +- name: Configure LDAP settings + community.docker.docker_container_exec: + container: "{{ nextcloud_service_name }}-nextcloud-1" + argv: + - php + - /var/www/html/occ + - ldap:set-config + - s01 + - "{{ item.key }}" + - "{{ item.value | string }}" + loop: "{{ nextcloud_ldap_config | dict2items }}" + loop_control: + label: "{{ item.key }}" + no_log: true + +- name: Test LDAP configuration + community.docker.docker_container_exec: + container: "{{ nextcloud_service_name }}-nextcloud-1" + command: php /var/www/html/occ ldap:test-config s01 + register: ldap_test_result + changed_when: false + failed_when: + - ldap_test_result.rc != 0 + - "'succeeded' not in (ldap_test_result.stdout | default('') | lower)" \ No newline at end of file diff --git a/roles/nextcloud/tasks/main.yml b/roles/nextcloud/tasks/main.yml index 1d1a565..71f68c5 100644 --- a/roles/nextcloud/tasks/main.yml +++ b/roles/nextcloud/tasks/main.yml @@ -70,6 +70,10 @@ ansible.builtin.include_tasks: collabora.yml when: nextcloud_enable_collabora +- name: Configure LDAP backend + ansible.builtin.include_tasks: ldap.yml + when: nextcloud_ldap_enabled + - name: Configure OIDC providers ansible.builtin.include_tasks: oidc.yml when: nextcloud_oidc_providers | length > 0 or nextcloud_oidc_providers_removed | length > 0 From 12864a13b09f2f913012fdc8dfa44b1038772382 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 13 Mar 2026 10:58:40 +0100 Subject: [PATCH 14/39] feat: add 389ds ldap backend to keycloak Signed-off-by: Bert-Jan Fikse --- roles/389ds/defaults/main.yml | 8 +++- roles/389ds/tasks/main.yml | 46 ++++++++++++++++++- roles/389ds/templates/base-ous.ldif.j2 | 7 +++ roles/389ds/templates/docker-compose.yml.j2 | 1 + roles/keycloak/defaults/main.yml | 31 +++++++++++++ roles/keycloak/tasks/main.yml | 2 + roles/keycloak/tasks/provisioning.yml | 33 +++++++++++++ .../keycloak/templates/docker-compose.yml.j2 | 12 +++++ 8 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 roles/389ds/templates/base-ous.ldif.j2 diff --git a/roles/389ds/defaults/main.yml b/roles/389ds/defaults/main.yml index 00bf5d1..82890ad 100644 --- a/roles/389ds/defaults/main.yml +++ b/roles/389ds/defaults/main.yml @@ -19,8 +19,14 @@ ds389_root_password: "changeme" # Instance configuration ds389_instance_name: "localhost" +ds389_hostname: "{{ ds389_service_name }}" # Network configuration ds389_backend_network: "backend" ds389_ldap_port: 3389 -ds389_ldaps_port: 3636 \ No newline at end of file +ds389_ldaps_port: 3636 + +# Base OUs to create after container starts +ds389_base_ous: + - users + - groups \ No newline at end of file diff --git a/roles/389ds/tasks/main.yml b/roles/389ds/tasks/main.yml index 277d2dd..117f12b 100644 --- a/roles/389ds/tasks/main.yml +++ b/roles/389ds/tasks/main.yml @@ -29,4 +29,48 @@ - name: Start 389ds container community.docker.docker_compose_v2: project_src: "{{ ds389_docker_compose_dir }}" - state: present \ No newline at end of file + state: present + +- name: Wait for LDAP to be ready + shell: > + docker compose -f {{ ds389_docker_compose_dir }}/docker-compose.yml + exec -T {{ ds389_service_name }} ldapsearch -H ldap://localhost:3389 -x + -D "{{ ds389_root_dn }}" -w "{{ ds389_root_password }}" + -b "" -s base "(objectClass=*)" + register: ds389_ldap_ready + retries: 30 + delay: 2 + until: ds389_ldap_ready.rc == 0 + changed_when: false + no_log: true + +- name: Ensure backend and suffix exist + shell: > + docker compose -f {{ ds389_docker_compose_dir }}/docker-compose.yml + exec -T {{ ds389_service_name }} dsconf localhost backend create + --suffix "{{ ds389_suffix }}" --be-name userroot --create-suffix + register: ds389_backend_result + failed_when: + - ds389_backend_result.rc != 0 + - "'already exists' not in ds389_backend_result.stderr" + - "'suffix exists' not in ds389_backend_result.stderr" + changed_when: ds389_backend_result.rc == 0 + +- name: Template base OUs LDIF + template: + src: base-ous.ldif.j2 + dest: "{{ ds389_docker_volume_dir }}/data/base-ous.ldif" + mode: '0644' + +- name: Apply base OUs LDIF + shell: > + docker compose -f {{ ds389_docker_compose_dir }}/docker-compose.yml + exec -T {{ ds389_service_name }} ldapadd -H ldap://localhost:3389 -x + -D "{{ ds389_root_dn }}" -w "{{ ds389_root_password }}" + -f /data/base-ous.ldif + register: ds389_ldapadd_result + failed_when: + - ds389_ldapadd_result.rc != 0 + - "'Already exists' not in ds389_ldapadd_result.stderr" + changed_when: "'Already exists' not in ds389_ldapadd_result.stderr" + no_log: true \ No newline at end of file diff --git a/roles/389ds/templates/base-ous.ldif.j2 b/roles/389ds/templates/base-ous.ldif.j2 new file mode 100644 index 0000000..8cccaa9 --- /dev/null +++ b/roles/389ds/templates/base-ous.ldif.j2 @@ -0,0 +1,7 @@ +{% for ou in ds389_base_ous %} +dn: ou={{ ou }},{{ ds389_suffix }} +changetype: add +objectClass: organizationalUnit +ou: {{ ou }} + +{% endfor %} diff --git a/roles/389ds/templates/docker-compose.yml.j2 b/roles/389ds/templates/docker-compose.yml.j2 index a8842e7..7e0c7c0 100644 --- a/roles/389ds/templates/docker-compose.yml.j2 +++ b/roles/389ds/templates/docker-compose.yml.j2 @@ -1,6 +1,7 @@ services: {{ ds389_service_name }}: image: {{ ds389_image }} + hostname: {{ ds389_hostname }} restart: unless-stopped environment: DS_SUFFIX_NAME: {{ ds389_suffix }} diff --git a/roles/keycloak/defaults/main.yml b/roles/keycloak/defaults/main.yml index c242ea5..df29e65 100644 --- a/roles/keycloak/defaults/main.yml +++ b/roles/keycloak/defaults/main.yml @@ -34,6 +34,14 @@ keycloak_log_level: "INFO" keycloak_proxy_mode: "edge" keycloak_gzip_enabled: false # Disable GZIP encoding to avoid MIME type issues +# Extra CA certificates to trust (host paths to PEM files) +keycloak_truststore_certificates: [] +# - /srv/data/389ds/data/ssca/ca.crt + +# Extra /etc/hosts entries for the Keycloak container +keycloak_extra_hosts: [] +# - "ldap:192.168.56.11" + # Provisioning configuration keycloak_provisioning_enabled: false @@ -96,3 +104,26 @@ keycloak_removed_clients: [] keycloak_removed_identity_providers: [] # - old-idp + +# LDAP user federations +keycloak_user_federations: [] +# - name: ldap-389ds +# provider_id: ldap +# config: +# editMode: WRITABLE +# syncRegistrations: "true" +# importEnabled: "true" +# vendor: rhds +# connectionUrl: "ldaps://ldap.example.com:636" +# usersDn: "ou=users,dc=example,dc=com" +# bindDn: "cn=Directory Manager" +# bindCredential: "changeme" +# usernameLDAPAttribute: uid +# rdnLDAPAttribute: uid +# uuidLDAPAttribute: nsuniqueid +# userObjectClasses: "inetOrgPerson, organizationalPerson" +# authType: simple +# useTruststoreSpi: never + +keycloak_removed_user_federations: [] +# - old-federation diff --git a/roles/keycloak/tasks/main.yml b/roles/keycloak/tasks/main.yml index f8a0f1e..33374a5 100644 --- a/roles/keycloak/tasks/main.yml +++ b/roles/keycloak/tasks/main.yml @@ -13,6 +13,8 @@ path: "{{ keycloak_docker_volume_dir }}/data" state: directory mode: '0755' + owner: "1000" + group: "1000" - name: Create postgres data directory file: diff --git a/roles/keycloak/tasks/provisioning.yml b/roles/keycloak/tasks/provisioning.yml index 03ad6df..97b53b2 100644 --- a/roles/keycloak/tasks/provisioning.yml +++ b/roles/keycloak/tasks/provisioning.yml @@ -30,6 +30,20 @@ loop: "{{ keycloak_removed_identity_providers }}" no_log: true +# Cleanup: Remove deleted user federations +- name: Remove deleted user federations + community.general.keycloak_user_federation: + auth_keycloak_url: "{{ keycloak_auth_url }}" + auth_realm: master + auth_username: "{{ keycloak_admin_user }}" + auth_password: "{{ keycloak_admin_password }}" + realm: "{{ keycloak_realm }}" + name: "{{ item }}" + state: absent + validate_certs: false + loop: "{{ keycloak_removed_user_federations }}" + no_log: true + # Cleanup: Remove deleted clients - name: Remove deleted clients community.general.keycloak_client: @@ -86,6 +100,25 @@ loop: "{{ keycloak_groups }}" no_log: true +# Create user federations (LDAP) +- name: Create user federations + community.general.keycloak_user_federation: + auth_keycloak_url: "{{ keycloak_auth_url }}" + auth_realm: master + auth_username: "{{ keycloak_admin_user }}" + auth_password: "{{ keycloak_admin_password }}" + realm: "{{ keycloak_realm }}" + name: "{{ item.name }}" + provider_id: "{{ item.provider_id }}" + provider_type: org.keycloak.storage.UserStorageProvider + config: "{{ item.config }}" + mappers: "{{ item.mappers | default(omit) }}" + bind_credential_update_mode: only_indirect + state: present + validate_certs: false + loop: "{{ keycloak_user_federations }}" + no_log: true + # Create local users - name: Create local users community.general.keycloak_user: diff --git a/roles/keycloak/templates/docker-compose.yml.j2 b/roles/keycloak/templates/docker-compose.yml.j2 index 2708f37..e08a2c7 100644 --- a/roles/keycloak/templates/docker-compose.yml.j2 +++ b/roles/keycloak/templates/docker-compose.yml.j2 @@ -33,13 +33,25 @@ services: KC_PROXY: {{ keycloak_proxy_mode }} KC_HOSTNAME: {{ keycloak_domain }} KC_HEALTH_ENABLED: "true" +{% if keycloak_truststore_certificates | length > 0 %} + KC_TRUSTSTORE_PATHS: "{{ keycloak_truststore_certificates | map('regex_replace', '^.*/(.*)$', '/opt/keycloak/certs/\\1') | join(',') }}" +{% endif %} depends_on: - postgres volumes: - {{ keycloak_docker_volume_dir }}/data:/opt/keycloak/data +{% for cert in keycloak_truststore_certificates %} + - {{ cert }}:/opt/keycloak/certs/{{ cert | basename }}:ro +{% endfor %} networks: - {{ keycloak_backend_network }} - {{ keycloak_traefik_network }} +{% if keycloak_extra_hosts | length > 0 %} + extra_hosts: +{% for host in keycloak_extra_hosts %} + - "{{ host }}" +{% endfor %} +{% endif %} tmpfs: - /opt/keycloak/data/tmp:size=1024m labels: From db21030a64ea9da55b7bfe6f5730e82f4eb4e942 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 13 Mar 2026 11:43:11 +0100 Subject: [PATCH 15/39] feat: add ldap backend to opencloud Signed-off-by: Bert-Jan Fikse --- roles/opencloud/defaults/main.yml | 18 +++++++++++++ .../opencloud/templates/docker-compose.yml.j2 | 25 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/roles/opencloud/defaults/main.yml b/roles/opencloud/defaults/main.yml index 9e43dcc..d7abfee 100644 --- a/roles/opencloud/defaults/main.yml +++ b/roles/opencloud/defaults/main.yml @@ -46,5 +46,23 @@ opencloud_collabora_domain: "" opencloud_wopi_domain: "" opencloud_collabora_insecure: true +# LDAP configuration (set opencloud_ldap_uri to enable external LDAP) +opencloud_ldap_uri: "" +opencloud_ldap_insecure: true +opencloud_ldap_bind_dn: "" +opencloud_ldap_bind_password: "" +opencloud_ldap_user_base_dn: "" +opencloud_ldap_group_base_dn: "" +opencloud_ldap_user_schema_id: "nsuniqueid" +opencloud_ldap_user_schema_id_is_octet_string: true +opencloud_ldap_user_schema_username: "uid" +opencloud_ldap_user_schema_mail: "mail" +opencloud_ldap_user_schema_display_name: "displayName" +opencloud_ldap_group_schema_id: "nsuniqueid" +opencloud_ldap_group_schema_id_is_octet_string: true +opencloud_ldap_group_schema_groupname: "cn" +opencloud_ldap_group_schema_member: "member" +opencloud_ldap_write_enabled: false + # CSP configuration (extra URLs to allow in connect-src) opencloud_csp_extra_connect_src: [] \ No newline at end of file diff --git a/roles/opencloud/templates/docker-compose.yml.j2 b/roles/opencloud/templates/docker-compose.yml.j2 index bc142a2..88faa46 100644 --- a/roles/opencloud/templates/docker-compose.yml.j2 +++ b/roles/opencloud/templates/docker-compose.yml.j2 @@ -44,6 +44,31 @@ services: STORAGE_USERS_DECOMPOSEDS3_SECRET_KEY: "{{ opencloud_s3_secret_key }}" STORAGE_USERS_DECOMPOSEDS3_BUCKET: "{{ opencloud_s3_bucket }}" {% endif %} +{% if opencloud_ldap_uri %} + # Disable built-in IDM when using external LDAP + OC_EXCLUDE_RUN_SERVICES: "idm" + IDM_CREATE_DEMO_USERS: "false" + # LDAP connection + OC_LDAP_URI: "{{ opencloud_ldap_uri }}" + OC_LDAP_INSECURE: "{{ opencloud_ldap_insecure | string | lower }}" + OC_LDAP_BIND_DN: "{{ opencloud_ldap_bind_dn }}" + OC_LDAP_BIND_PASSWORD: "{{ opencloud_ldap_bind_password }}" + # LDAP user/group base + OC_LDAP_USER_BASE_DN: "{{ opencloud_ldap_user_base_dn }}" + OC_LDAP_GROUP_BASE_DN: "{{ opencloud_ldap_group_base_dn }}" + # LDAP user schema + OC_LDAP_USER_SCHEMA_ID: "{{ opencloud_ldap_user_schema_id }}" + OC_LDAP_USER_SCHEMA_ID_IS_OCTET_STRING: "{{ opencloud_ldap_user_schema_id_is_octet_string | string | lower }}" + OC_LDAP_USER_SCHEMA_USERNAME: "{{ opencloud_ldap_user_schema_username }}" + OC_LDAP_USER_SCHEMA_MAIL: "{{ opencloud_ldap_user_schema_mail }}" + OC_LDAP_USER_SCHEMA_DISPLAY_NAME: "{{ opencloud_ldap_user_schema_display_name }}" + # LDAP group schema + OC_LDAP_GROUP_SCHEMA_ID: "{{ opencloud_ldap_group_schema_id }}" + OC_LDAP_GROUP_SCHEMA_ID_IS_OCTET_STRING: "{{ opencloud_ldap_group_schema_id_is_octet_string | string | lower }}" + OC_LDAP_GROUP_SCHEMA_GROUPNAME: "{{ opencloud_ldap_group_schema_groupname }}" + OC_LDAP_GROUP_SCHEMA_MEMBER: "{{ opencloud_ldap_group_schema_member }}" + GRAPH_LDAP_SERVER_WRITE_ENABLED: "{{ opencloud_ldap_write_enabled | string | lower }}" +{% endif %} {% if opencloud_collabora_domain %} OC_ADD_RUN_SERVICES: "collaboration" COLLABORA_DOMAIN: "{{ opencloud_collabora_domain }}" From f3f2b6d5b7e6dd082cd6485241c6aaf78d4fe7c5 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 13 Mar 2026 13:44:53 +0100 Subject: [PATCH 16/39] feat: add empty role skeleton for drawio role Signed-off-by: Bert-Jan Fikse --- roles/drawio/README.md | 38 ++++++++++++++++++++++++++++++++++ roles/drawio/defaults/main.yml | 3 +++ roles/drawio/handlers/main.yml | 3 +++ roles/drawio/meta/main.yml | 35 +++++++++++++++++++++++++++++++ roles/drawio/tasks/main.yml | 3 +++ roles/drawio/tests/inventory | 3 +++ roles/drawio/tests/test.yml | 6 ++++++ roles/drawio/vars/main.yml | 3 +++ 8 files changed, 94 insertions(+) create mode 100644 roles/drawio/README.md create mode 100644 roles/drawio/defaults/main.yml create mode 100644 roles/drawio/handlers/main.yml create mode 100644 roles/drawio/meta/main.yml create mode 100644 roles/drawio/tasks/main.yml create mode 100644 roles/drawio/tests/inventory create mode 100644 roles/drawio/tests/test.yml create mode 100644 roles/drawio/vars/main.yml diff --git a/roles/drawio/README.md b/roles/drawio/README.md new file mode 100644 index 0000000..225dd44 --- /dev/null +++ b/roles/drawio/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/roles/drawio/defaults/main.yml b/roles/drawio/defaults/main.yml new file mode 100644 index 0000000..dcb88be --- /dev/null +++ b/roles/drawio/defaults/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# defaults file for drawio diff --git a/roles/drawio/handlers/main.yml b/roles/drawio/handlers/main.yml new file mode 100644 index 0000000..7bdf858 --- /dev/null +++ b/roles/drawio/handlers/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# handlers file for drawio diff --git a/roles/drawio/meta/main.yml b/roles/drawio/meta/main.yml new file mode 100644 index 0000000..6f91fd3 --- /dev/null +++ b/roles/drawio/meta/main.yml @@ -0,0 +1,35 @@ +#SPDX-License-Identifier: MIT-0 +galaxy_info: + author: your name + description: your role description + company: your company (optional) + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.2 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/roles/drawio/tasks/main.yml b/roles/drawio/tasks/main.yml new file mode 100644 index 0000000..a3bc871 --- /dev/null +++ b/roles/drawio/tasks/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# tasks file for drawio diff --git a/roles/drawio/tests/inventory b/roles/drawio/tests/inventory new file mode 100644 index 0000000..03ca42f --- /dev/null +++ b/roles/drawio/tests/inventory @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +localhost + diff --git a/roles/drawio/tests/test.yml b/roles/drawio/tests/test.yml new file mode 100644 index 0000000..b542b76 --- /dev/null +++ b/roles/drawio/tests/test.yml @@ -0,0 +1,6 @@ +#SPDX-License-Identifier: MIT-0 +--- +- hosts: localhost + remote_user: root + roles: + - drawio diff --git a/roles/drawio/vars/main.yml b/roles/drawio/vars/main.yml new file mode 100644 index 0000000..245172f --- /dev/null +++ b/roles/drawio/vars/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# vars file for drawio From 910986b808bdfb5977a70f6fd4320b8701208df7 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 13 Mar 2026 14:37:02 +0100 Subject: [PATCH 17/39] feat: add drawio instance for nextcloud and opencloud Signed-off-by: Bert-Jan Fikse --- roles/drawio/defaults/main.yml | 17 +++++++++++ roles/drawio/handlers/main.yml | 5 ++++ roles/drawio/tasks/main.yml | 18 ++++++++++++ roles/drawio/templates/docker-compose.yml.j2 | 28 +++++++++++++++++++ roles/nextcloud/defaults/main.yml | 6 ++++ roles/nextcloud/tasks/drawio.yml | 19 +++++++++++++ roles/nextcloud/tasks/main.yml | 4 +++ roles/opencloud/defaults/main.yml | 10 +++++-- roles/opencloud/tasks/main.yml | 27 +++++++++++++++++- .../opencloud/templates/csp-override.yaml.j2 | 7 +++++ .../opencloud/templates/docker-compose.yml.j2 | 15 +++++++++- 11 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 roles/drawio/templates/docker-compose.yml.j2 create mode 100644 roles/nextcloud/tasks/drawio.yml diff --git a/roles/drawio/defaults/main.yml b/roles/drawio/defaults/main.yml index dcb88be..7b67976 100644 --- a/roles/drawio/defaults/main.yml +++ b/roles/drawio/defaults/main.yml @@ -1,3 +1,20 @@ #SPDX-License-Identifier: MIT-0 --- # defaults file for drawio + +# Base directory configuration (inherited from base role or defined here) +docker_compose_base_dir: /etc/docker/compose + +# Drawio-specific configuration +drawio_service_name: drawio +drawio_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ drawio_service_name }}" + +# Service configuration +drawio_domain: "drawio.local.test" +drawio_image: "jgraph/drawio:latest" +drawio_port: 8080 +drawio_extra_hosts: [] + +# Traefik configuration +drawio_traefik_network: "proxy" +drawio_use_ssl: true \ No newline at end of file diff --git a/roles/drawio/handlers/main.yml b/roles/drawio/handlers/main.yml index 7bdf858..f1ef0da 100644 --- a/roles/drawio/handlers/main.yml +++ b/roles/drawio/handlers/main.yml @@ -1,3 +1,8 @@ #SPDX-License-Identifier: MIT-0 --- # handlers file for drawio + +- name: restart drawio + community.docker.docker_compose_v2: + project_src: "{{ drawio_docker_compose_dir }}" + state: restarted \ No newline at end of file diff --git a/roles/drawio/tasks/main.yml b/roles/drawio/tasks/main.yml index a3bc871..67bd50d 100644 --- a/roles/drawio/tasks/main.yml +++ b/roles/drawio/tasks/main.yml @@ -1,3 +1,21 @@ #SPDX-License-Identifier: MIT-0 --- # tasks file for drawio + +- name: Create docker compose directory + file: + path: "{{ drawio_docker_compose_dir }}" + state: directory + mode: '0755' + +- name: Create docker-compose file for drawio + template: + src: docker-compose.yml.j2 + dest: "{{ drawio_docker_compose_dir }}/docker-compose.yml" + mode: '0644' + notify: restart drawio + +- name: Start drawio container + community.docker.docker_compose_v2: + project_src: "{{ drawio_docker_compose_dir }}" + state: present \ No newline at end of file diff --git a/roles/drawio/templates/docker-compose.yml.j2 b/roles/drawio/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..b6b9ef5 --- /dev/null +++ b/roles/drawio/templates/docker-compose.yml.j2 @@ -0,0 +1,28 @@ +services: + drawio: + image: {{ drawio_image }} + container_name: {{ drawio_service_name }} + restart: unless-stopped + networks: + - {{ drawio_traefik_network }} +{% if drawio_extra_hosts is defined and drawio_extra_hosts | length > 0 %} + extra_hosts: +{% for host in drawio_extra_hosts %} + - "{{ host }}" +{% endfor %} +{% endif %} + labels: + - traefik.enable=true + - traefik.docker.network={{ drawio_traefik_network }} + - traefik.http.routers.{{ drawio_service_name }}.rule=Host(`{{ drawio_domain }}`) + - traefik.http.services.{{ drawio_service_name }}.loadbalancer.server.port={{ drawio_port }} +{% if drawio_use_ssl %} + - traefik.http.routers.{{ drawio_service_name }}.entrypoints=websecure + - traefik.http.routers.{{ drawio_service_name }}.tls=true +{% else %} + - traefik.http.routers.{{ drawio_service_name }}.entrypoints=web +{% endif %} + +networks: + {{ drawio_traefik_network }}: + external: true \ No newline at end of file diff --git a/roles/nextcloud/defaults/main.yml b/roles/nextcloud/defaults/main.yml index e40ea55..ddafddf 100644 --- a/roles/nextcloud/defaults/main.yml +++ b/roles/nextcloud/defaults/main.yml @@ -29,6 +29,12 @@ nextcloud_enable_collabora: true nextcloud_collabora_domain: "office.local.test" nextcloud_collabora_disable_cert_verification: false +# Draw.io integration (set nextcloud_drawio_url to enable) +nextcloud_enable_drawio: false +nextcloud_drawio_url: "" +nextcloud_drawio_theme: "kennedy" +nextcloud_drawio_offline: "yes" + nextcloud_use_s3_storage: false nextcloud_s3_key: changeme nextcloud_s3_secret: changeme diff --git a/roles/nextcloud/tasks/drawio.yml b/roles/nextcloud/tasks/drawio.yml new file mode 100644 index 0000000..bd2e17e --- /dev/null +++ b/roles/nextcloud/tasks/drawio.yml @@ -0,0 +1,19 @@ +#SPDX-License-Identifier: MIT-0 +--- +# tasks file for configuring draw.io in Nextcloud + +- name: Configure draw.io URL + community.docker.docker_container_exec: + container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1" + command: php /var/www/html/occ config:app:set drawio DrawioUrl --value={{ nextcloud_drawio_url }} + when: nextcloud_drawio_url | length > 0 + +- name: Configure draw.io theme + community.docker.docker_container_exec: + container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1" + command: php /var/www/html/occ config:app:set drawio DrawioTheme --value={{ nextcloud_drawio_theme }} + +- name: Configure draw.io offline mode + community.docker.docker_container_exec: + container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1" + command: php /var/www/html/occ config:app:set drawio DrawioOffline --value={{ nextcloud_drawio_offline }} \ No newline at end of file diff --git a/roles/nextcloud/tasks/main.yml b/roles/nextcloud/tasks/main.yml index 71f68c5..c849b17 100644 --- a/roles/nextcloud/tasks/main.yml +++ b/roles/nextcloud/tasks/main.yml @@ -70,6 +70,10 @@ ansible.builtin.include_tasks: collabora.yml when: nextcloud_enable_collabora +- name: Configure nextcloud draw.io + ansible.builtin.include_tasks: drawio.yml + when: nextcloud_enable_drawio + - name: Configure LDAP backend ansible.builtin.include_tasks: ldap.yml when: nextcloud_ldap_enabled diff --git a/roles/opencloud/defaults/main.yml b/roles/opencloud/defaults/main.yml index d7abfee..a939618 100644 --- a/roles/opencloud/defaults/main.yml +++ b/roles/opencloud/defaults/main.yml @@ -64,5 +64,11 @@ opencloud_ldap_group_schema_groupname: "cn" opencloud_ldap_group_schema_member: "member" opencloud_ldap_write_enabled: false -# CSP configuration (extra URLs to allow in connect-src) -opencloud_csp_extra_connect_src: [] \ No newline at end of file +# Draw.io integration (set opencloud_drawio_url to enable) +opencloud_drawio_url: "" +opencloud_drawio_theme: "minimal" +opencloud_drawio_extension_image: "opencloudeu/web-extensions:draw-io-latest" + +# CSP configuration (extra URLs to allow in connect-src and frame-src) +opencloud_csp_extra_connect_src: [] +opencloud_csp_extra_frame_src: [] \ No newline at end of file diff --git a/roles/opencloud/tasks/main.yml b/roles/opencloud/tasks/main.yml index d3ce5ba..e448bb9 100644 --- a/roles/opencloud/tasks/main.yml +++ b/roles/opencloud/tasks/main.yml @@ -31,7 +31,32 @@ owner: "1000" group: "1000" mode: '0644' - when: opencloud_csp_extra_connect_src | length > 0 + when: opencloud_csp_extra_connect_src | length > 0 or opencloud_csp_extra_frame_src | length > 0 + notify: restart opencloud + +- name: Create draw.io extension apps directory + file: + path: "{{ opencloud_docker_volume_dir }}/data/web/assets/apps/draw-io" + state: directory + owner: "1000" + group: "1000" + mode: '0755' + when: opencloud_drawio_url | length > 0 + +- name: Create draw.io extension config + copy: + content: | + { + "config": { + "url": "{{ opencloud_drawio_url }}", + "theme": "{{ opencloud_drawio_theme }}" + } + } + dest: "{{ opencloud_docker_volume_dir }}/data/web/assets/apps/draw-io/config.json" + owner: "1000" + group: "1000" + mode: '0644' + when: opencloud_drawio_url | length > 0 notify: restart opencloud - name: Create docker-compose file for opencloud diff --git a/roles/opencloud/templates/csp-override.yaml.j2 b/roles/opencloud/templates/csp-override.yaml.j2 index f71cd9b..29afd38 100644 --- a/roles/opencloud/templates/csp-override.yaml.j2 +++ b/roles/opencloud/templates/csp-override.yaml.j2 @@ -7,6 +7,13 @@ directives: {% for url in opencloud_csp_extra_connect_src %} - "{{ url }}" {% endfor %} +{% if opencloud_csp_extra_frame_src | length > 0 %} + frame-src: + - "'self'" +{% for url in opencloud_csp_extra_frame_src %} + - "{{ url }}" +{% endfor %} +{% endif %} script-src: - "'self'" - "'unsafe-inline'" diff --git a/roles/opencloud/templates/docker-compose.yml.j2 b/roles/opencloud/templates/docker-compose.yml.j2 index 88faa46..eca62b3 100644 --- a/roles/opencloud/templates/docker-compose.yml.j2 +++ b/roles/opencloud/templates/docker-compose.yml.j2 @@ -1,8 +1,21 @@ services: +{% if opencloud_drawio_url %} + drawio-ext: + image: {{ opencloud_drawio_extension_image }} + entrypoint: /bin/sh + command: ["-c", "cp -R /usr/share/nginx/html/apps/draw-io/ /apps/"] + volumes: + - {{ opencloud_docker_volume_dir }}/data/web/assets/apps:/apps +{% endif %} opencloud: image: {{ opencloud_image }} container_name: {{ opencloud_service_name }} restart: unless-stopped +{% if opencloud_drawio_url %} + depends_on: + drawio-ext: + condition: service_completed_successfully +{% endif %} entrypoint: - /bin/sh command: ["-c", "opencloud init || true; opencloud server"] @@ -18,7 +31,7 @@ services: OC_INSECURE: "true" OC_LOG_LEVEL: "{{ opencloud_log_level }}" PROXY_TLS: "false" -{% if opencloud_csp_extra_connect_src | length > 0 %} +{% if opencloud_csp_extra_connect_src | length > 0 or opencloud_csp_extra_frame_src | length > 0 %} PROXY_CSP_CONFIG_FILE_OVERRIDE_LOCATION: "/etc/opencloud/csp-override.yaml" {% endif %} IDM_ADMIN_PASSWORD: "{{ opencloud_admin_password }}" From d517f77b6ceb7a76fb17b1054ee4d2ce760d3b39 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 13 Mar 2026 15:22:09 +0100 Subject: [PATCH 18/39] feat: add file_lock and notify_push configuration to nextcloud role Signed-off-by: Bert-Jan Fikse --- roles/nextcloud/defaults/main.yml | 8 ++++ roles/nextcloud/tasks/main.yml | 4 ++ roles/nextcloud/tasks/notify_push.yml | 8 ++++ .../nextcloud/templates/docker-compose.yml.j2 | 37 ++++++++++++++++++- 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 roles/nextcloud/tasks/notify_push.yml diff --git a/roles/nextcloud/defaults/main.yml b/roles/nextcloud/defaults/main.yml index ddafddf..a437ce6 100644 --- a/roles/nextcloud/defaults/main.yml +++ b/roles/nextcloud/defaults/main.yml @@ -53,6 +53,12 @@ nextcloud_upload_limit_mb: 2048 nextcloud_scale_factor: 2 +# Trusted proxies (Docker internal networks) +nextcloud_trusted_proxies: "172.16.0.0/12" + +# File locking and real-time push notifications +nextcloud_enable_notify_push: false + # Non-default apps to install and enable nextcloud_apps_to_install: - groupfolders @@ -61,6 +67,8 @@ nextcloud_apps_to_install: - user_ldap - user_oidc - whiteboard + - files_lock + - notify_push # OIDC provider configuration nextcloud_oidc_allow_selfsigned: false # Set to true to disable SSL verification for OIDC providers (dev only) diff --git a/roles/nextcloud/tasks/main.yml b/roles/nextcloud/tasks/main.yml index c849b17..530baf7 100644 --- a/roles/nextcloud/tasks/main.yml +++ b/roles/nextcloud/tasks/main.yml @@ -74,6 +74,10 @@ ansible.builtin.include_tasks: drawio.yml when: nextcloud_enable_drawio +- name: Configure notify_push + ansible.builtin.include_tasks: notify_push.yml + when: nextcloud_enable_notify_push + - name: Configure LDAP backend ansible.builtin.include_tasks: ldap.yml when: nextcloud_ldap_enabled diff --git a/roles/nextcloud/tasks/notify_push.yml b/roles/nextcloud/tasks/notify_push.yml new file mode 100644 index 0000000..18dbb8b --- /dev/null +++ b/roles/nextcloud/tasks/notify_push.yml @@ -0,0 +1,8 @@ +#SPDX-License-Identifier: MIT-0 +--- +# tasks file for configuring notify_push in Nextcloud + +- name: Configure notify_push base endpoint + community.docker.docker_container_exec: + container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1" + command: php /var/www/html/occ notify_push:setup https://{{ nextcloud_domain }}/push \ No newline at end of file diff --git a/roles/nextcloud/templates/docker-compose.yml.j2 b/roles/nextcloud/templates/docker-compose.yml.j2 index 3ae9fd5..9a98033 100644 --- a/roles/nextcloud/templates/docker-compose.yml.j2 +++ b/roles/nextcloud/templates/docker-compose.yml.j2 @@ -61,7 +61,7 @@ services: PHP_UPLOAD_LIMIT: {{ nextcloud_upload_limit_mb }}M OVERWRITEPROTOCOL: https OVERWRITEHOST: {{ nextcloud_domain }} - TRUSTED_PROXIES: "172.18.0.0/16 172.16.9.88/16 172.16.17.0/24 172.16.9.88" + TRUSTED_PROXIES: "{{ nextcloud_trusted_proxies }}" volumes: - {{ nextcloud_docker_volume_dir }}/nextcloud/:/var/www/html networks: @@ -86,7 +86,7 @@ services: PHP_UPLOAD_LIMIT: {{ nextcloud_upload_limit_mb }}M OVERWRITEPROTOCOL: https OVERWRITEHOST: {{ nextcloud_domain }} - TRUSTED_PROXIES: "172.18.0.0/16 172.16.9.88/16 172.16.17.0/24 172.16.9.88" + TRUSTED_PROXIES: "{{ nextcloud_trusted_proxies }}" {% if nextcloud_use_s3_storage %} OBJECTSTORE_S3_KEY: {{ nextcloud_s3_key }} OBJECTSTORE_S3_SECRET: {{ nextcloud_s3_secret }} @@ -109,6 +109,39 @@ services: {% endfor %} {% endif %} +{% if nextcloud_enable_notify_push %} + notify-push: + image: icewind1991/notify_push + restart: always + depends_on: + - redis + - db + volumes: + - {{ nextcloud_docker_volume_dir }}/nextcloud/:/var/www/html + environment: + PORT: "7867" + REDIS_URL: "redis://redis:6379" + DATABASE_URL: "postgres://{{ nextcloud_postgres_user }}:{{ nextcloud_postgres_password }}@db:5432/{{ nextcloud_postgres_db }}" + DATABASE_PREFIX: "oc_" + NEXTCLOUD_URL: "http://nginx" + networks: + - {{ nextcloud_backend_network }} + - {{ nextcloud_traefik_network }} + labels: + - traefik.enable=true + - traefik.docker.network={{ nextcloud_traefik_network }} + - traefik.http.routers.{{ nextcloud_service_name }}-push.rule=Host(`{{ nextcloud_domain }}`) && PathPrefix(`/push`) + - traefik.http.services.{{ nextcloud_service_name }}-push.loadbalancer.server.port=7867 +{% if nextcloud_use_ssl %} + - traefik.http.routers.{{ nextcloud_service_name }}-push.entrypoints=websecure + - traefik.http.routers.{{ nextcloud_service_name }}-push.tls=true +{% else %} + - traefik.http.routers.{{ nextcloud_service_name }}-push.entrypoints=web +{% endif %} + - traefik.http.middlewares.{{ nextcloud_service_name }}-push-https.headers.customrequestheaders.X-Forwarded-Proto=https + - traefik.http.routers.{{ nextcloud_service_name }}-push.middlewares={{ nextcloud_service_name }}-push-https +{% endif %} + networks: {{ nextcloud_backend_network }}: {{ nextcloud_traefik_network }}: From 6f4cc2bdb3cbc715bcf0d03d1732f6d818e1ca3a Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 13 Mar 2026 15:37:33 +0100 Subject: [PATCH 19/39] feat: nextcloud ability to get groups from ldap backend Signed-off-by: Bert-Jan Fikse --- roles/nextcloud/defaults/main.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/roles/nextcloud/defaults/main.yml b/roles/nextcloud/defaults/main.yml index a437ce6..0adf71e 100644 --- a/roles/nextcloud/defaults/main.yml +++ b/roles/nextcloud/defaults/main.yml @@ -111,4 +111,10 @@ nextcloud_ldap_config: {} # ldapEmailAttribute: "mail" # ldapExpertUsernameAttr: "uid" # ldapExpertUUIDUserAttr: "nsuniqueid" +# ldapBaseGroups: "ou=groups,dc=example,dc=com" +# ldapGroupFilter: "(&(objectClass=groupOfNames))" +# ldapGroupFilterObjectclass: "groupOfNames" +# ldapGroupDisplayName: "cn" +# ldapGroupMemberAssocAttr: "member" +# ldapAdminGroup: "admins" # ldapConfigurationActive: "1" \ No newline at end of file From aa8baad630109b25ed55c2087046496e645445df Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 13 Mar 2026 16:43:02 +0100 Subject: [PATCH 20/39] feat: opencloud group provisioning via oidc Signed-off-by: Bert-Jan Fikse --- roles/keycloak/tasks/provisioning.yml | 1 + roles/opencloud/defaults/main.yml | 11 +++++++++++ roles/opencloud/tasks/main.yml | 10 ++++++++++ roles/opencloud/templates/docker-compose.yml.j2 | 6 ++++++ roles/opencloud/templates/proxy.yaml.j2 | 9 +++++++++ 5 files changed, 37 insertions(+) create mode 100644 roles/opencloud/templates/proxy.yaml.j2 diff --git a/roles/keycloak/tasks/provisioning.yml b/roles/keycloak/tasks/provisioning.yml index 97b53b2..f1d915a 100644 --- a/roles/keycloak/tasks/provisioning.yml +++ b/roles/keycloak/tasks/provisioning.yml @@ -163,6 +163,7 @@ direct_access_grants_enabled: "{{ item.direct_access_grants_enabled | default(false) }}" protocol: openid-connect default_client_scopes: "{{ item.default_client_scopes | default(['openid', 'email', 'profile']) }}" + protocol_mappers: "{{ item.protocol_mappers | default(omit) }}" state: present validate_certs: false loop: "{{ keycloak_oidc_clients }}" diff --git a/roles/opencloud/defaults/main.yml b/roles/opencloud/defaults/main.yml index a939618..137ece8 100644 --- a/roles/opencloud/defaults/main.yml +++ b/roles/opencloud/defaults/main.yml @@ -64,6 +64,17 @@ opencloud_ldap_group_schema_groupname: "cn" opencloud_ldap_group_schema_member: "member" opencloud_ldap_write_enabled: false +# Role assignment via OIDC (set opencloud_role_assignment_driver to "oidc" to enable) +opencloud_role_assignment_driver: "default" +opencloud_role_assignment_oidc_claim: "groups" +opencloud_role_mapping: [] +# Example mapping LDAP groups to OpenCloud roles: +# opencloud_role_mapping: +# - role_name: admin +# claim_value: admins +# - role_name: user +# claim_value: users + # Draw.io integration (set opencloud_drawio_url to enable) opencloud_drawio_url: "" opencloud_drawio_theme: "minimal" diff --git a/roles/opencloud/tasks/main.yml b/roles/opencloud/tasks/main.yml index e448bb9..9de9625 100644 --- a/roles/opencloud/tasks/main.yml +++ b/roles/opencloud/tasks/main.yml @@ -34,6 +34,16 @@ when: opencloud_csp_extra_connect_src | length > 0 or opencloud_csp_extra_frame_src | length > 0 notify: restart opencloud +- name: Create proxy role assignment config + template: + src: proxy.yaml.j2 + dest: "{{ opencloud_docker_volume_dir }}/config/proxy.yaml" + owner: "1000" + group: "1000" + mode: '0644' + when: opencloud_role_assignment_driver == "oidc" and opencloud_role_mapping | length > 0 + notify: restart opencloud + - name: Create draw.io extension apps directory file: path: "{{ opencloud_docker_volume_dir }}/data/web/assets/apps/draw-io" diff --git a/roles/opencloud/templates/docker-compose.yml.j2 b/roles/opencloud/templates/docker-compose.yml.j2 index eca62b3..10d8d22 100644 --- a/roles/opencloud/templates/docker-compose.yml.j2 +++ b/roles/opencloud/templates/docker-compose.yml.j2 @@ -35,6 +35,12 @@ services: PROXY_CSP_CONFIG_FILE_OVERRIDE_LOCATION: "/etc/opencloud/csp-override.yaml" {% endif %} IDM_ADMIN_PASSWORD: "{{ opencloud_admin_password }}" +{% if opencloud_role_assignment_driver == "oidc" %} + PROXY_ROLE_ASSIGNMENT_DRIVER: "oidc" + PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM: "{{ opencloud_role_assignment_oidc_claim }}" + GRAPH_ASSIGN_DEFAULT_USER_ROLE: "false" + SETTINGS_SETUP_DEFAULT_ASSIGNMENTS: "false" +{% endif %} {% if opencloud_oidc_issuer %} OC_OIDC_ISSUER: "{{ opencloud_oidc_issuer }}" OC_OIDC_CLIENT_ID: "{{ opencloud_oidc_client_id }}" diff --git a/roles/opencloud/templates/proxy.yaml.j2 b/roles/opencloud/templates/proxy.yaml.j2 new file mode 100644 index 0000000..78f5a9e --- /dev/null +++ b/roles/opencloud/templates/proxy.yaml.j2 @@ -0,0 +1,9 @@ +role_assignment: + driver: oidc + oidc_role_mapper: + role_claim: {{ opencloud_role_assignment_oidc_claim }} + role_mapping: +{% for mapping in opencloud_role_mapping %} + - role_name: {{ mapping.role_name }} + claim_value: "{{ mapping.claim_value }}" +{% endfor %} \ No newline at end of file From 77484f194460e041e5b69f2dfbfe6c512ae4ad7d Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Thu, 2 Apr 2026 11:51:02 +0200 Subject: [PATCH 21/39] chore: add new empty role skeleton for authentik_outpost_ldap Signed-off-by: Bert-Jan Fikse --- roles/authentik_outpost_ldap/README.md | 38 +++++++++++++++++++ .../authentik_outpost_ldap/defaults/main.yml | 3 ++ .../authentik_outpost_ldap/handlers/main.yml | 3 ++ roles/authentik_outpost_ldap/meta/main.yml | 35 +++++++++++++++++ roles/authentik_outpost_ldap/tasks/main.yml | 3 ++ roles/authentik_outpost_ldap/tests/inventory | 3 ++ roles/authentik_outpost_ldap/tests/test.yml | 6 +++ roles/authentik_outpost_ldap/vars/main.yml | 3 ++ 8 files changed, 94 insertions(+) create mode 100644 roles/authentik_outpost_ldap/README.md create mode 100644 roles/authentik_outpost_ldap/defaults/main.yml create mode 100644 roles/authentik_outpost_ldap/handlers/main.yml create mode 100644 roles/authentik_outpost_ldap/meta/main.yml create mode 100644 roles/authentik_outpost_ldap/tasks/main.yml create mode 100644 roles/authentik_outpost_ldap/tests/inventory create mode 100644 roles/authentik_outpost_ldap/tests/test.yml create mode 100644 roles/authentik_outpost_ldap/vars/main.yml diff --git a/roles/authentik_outpost_ldap/README.md b/roles/authentik_outpost_ldap/README.md new file mode 100644 index 0000000..225dd44 --- /dev/null +++ b/roles/authentik_outpost_ldap/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/roles/authentik_outpost_ldap/defaults/main.yml b/roles/authentik_outpost_ldap/defaults/main.yml new file mode 100644 index 0000000..0222b44 --- /dev/null +++ b/roles/authentik_outpost_ldap/defaults/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# defaults file for authentik_outpost_ldap diff --git a/roles/authentik_outpost_ldap/handlers/main.yml b/roles/authentik_outpost_ldap/handlers/main.yml new file mode 100644 index 0000000..cf99ed8 --- /dev/null +++ b/roles/authentik_outpost_ldap/handlers/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# handlers file for authentik_outpost_ldap diff --git a/roles/authentik_outpost_ldap/meta/main.yml b/roles/authentik_outpost_ldap/meta/main.yml new file mode 100644 index 0000000..6f91fd3 --- /dev/null +++ b/roles/authentik_outpost_ldap/meta/main.yml @@ -0,0 +1,35 @@ +#SPDX-License-Identifier: MIT-0 +galaxy_info: + author: your name + description: your role description + company: your company (optional) + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.2 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/roles/authentik_outpost_ldap/tasks/main.yml b/roles/authentik_outpost_ldap/tasks/main.yml new file mode 100644 index 0000000..36d90a4 --- /dev/null +++ b/roles/authentik_outpost_ldap/tasks/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# tasks file for authentik_outpost_ldap diff --git a/roles/authentik_outpost_ldap/tests/inventory b/roles/authentik_outpost_ldap/tests/inventory new file mode 100644 index 0000000..03ca42f --- /dev/null +++ b/roles/authentik_outpost_ldap/tests/inventory @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +localhost + diff --git a/roles/authentik_outpost_ldap/tests/test.yml b/roles/authentik_outpost_ldap/tests/test.yml new file mode 100644 index 0000000..597e16f --- /dev/null +++ b/roles/authentik_outpost_ldap/tests/test.yml @@ -0,0 +1,6 @@ +#SPDX-License-Identifier: MIT-0 +--- +- hosts: localhost + remote_user: root + roles: + - authentik_outpost_ldap diff --git a/roles/authentik_outpost_ldap/vars/main.yml b/roles/authentik_outpost_ldap/vars/main.yml new file mode 100644 index 0000000..dd3a45b --- /dev/null +++ b/roles/authentik_outpost_ldap/vars/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# vars file for authentik_outpost_ldap From 468ed3455032a48529d17787ddf83a8fc93f1f55 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 10 Apr 2026 11:16:56 +0200 Subject: [PATCH 22/39] feat: ability to set extra networks for nextcloud needed for ldap outpost Signed-off-by: Bert-Jan Fikse --- roles/nextcloud/defaults/main.yml | 1 + roles/nextcloud/tasks/main.yml | 6 ++++++ roles/nextcloud/templates/docker-compose.yml.j2 | 12 +++++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/roles/nextcloud/defaults/main.yml b/roles/nextcloud/defaults/main.yml index 0adf71e..478bfb7 100644 --- a/roles/nextcloud/defaults/main.yml +++ b/roles/nextcloud/defaults/main.yml @@ -14,6 +14,7 @@ nextcloud_image: "nextcloud:fpm" nextcloud_redis_image: "redis:latest" nextcloud_port: 80 nextcloud_extra_hosts: [] +nextcloud_extra_networks: [] nextcloud_allow_local_remote_servers: false # Set to true to allow requests to local network (dev only) nextcloud_postgres_image: "postgres:15" diff --git a/roles/nextcloud/tasks/main.yml b/roles/nextcloud/tasks/main.yml index 530baf7..8d2a5cd 100644 --- a/roles/nextcloud/tasks/main.yml +++ b/roles/nextcloud/tasks/main.yml @@ -19,6 +19,12 @@ state: directory mode: '0755' +- name: Ensure extra networks exist + community.docker.docker_network: + name: "{{ item }}" + state: present + loop: "{{ nextcloud_extra_networks }}" + - name: Create docker-compose file for nextcloud template: src: docker-compose.yml.j2 diff --git a/roles/nextcloud/templates/docker-compose.yml.j2 b/roles/nextcloud/templates/docker-compose.yml.j2 index 9a98033..fc86bdd 100644 --- a/roles/nextcloud/templates/docker-compose.yml.j2 +++ b/roles/nextcloud/templates/docker-compose.yml.j2 @@ -66,6 +66,9 @@ services: - {{ nextcloud_docker_volume_dir }}/nextcloud/:/var/www/html networks: - {{ nextcloud_backend_network }} +{% for net in nextcloud_extra_networks %} + - {{ net }} +{% endfor %} nextcloud: image: {{ nextcloud_image }} @@ -102,6 +105,9 @@ services: - {{ nextcloud_docker_volume_dir }}/nextcloud/:/var/www/html networks: - {{ nextcloud_backend_network }} +{% for net in nextcloud_extra_networks %} + - {{ net }} +{% endfor %} {% if nextcloud_extra_hosts is defined and nextcloud_extra_hosts | length > 0 %} extra_hosts: {% for host in nextcloud_extra_hosts %} @@ -145,4 +151,8 @@ services: networks: {{ nextcloud_backend_network }}: {{ nextcloud_traefik_network }}: - external: true \ No newline at end of file + external: true +{% for net in nextcloud_extra_networks %} + {{ net }}: + external: true +{% endfor %} \ No newline at end of file From e2fae25592345999aebb13a36d9c6e3dccc31895 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 10 Apr 2026 11:18:28 +0200 Subject: [PATCH 23/39] feat: make nextcloud_notify_push_image configurable Signed-off-by: Bert-Jan Fikse --- roles/nextcloud/defaults/main.yml | 1 + roles/nextcloud/templates/docker-compose.yml.j2 | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/roles/nextcloud/defaults/main.yml b/roles/nextcloud/defaults/main.yml index 478bfb7..7535b5a 100644 --- a/roles/nextcloud/defaults/main.yml +++ b/roles/nextcloud/defaults/main.yml @@ -59,6 +59,7 @@ nextcloud_trusted_proxies: "172.16.0.0/12" # File locking and real-time push notifications nextcloud_enable_notify_push: false +nextcloud_notify_push_image: "icewind1991/notify_push:1.3.1" # Non-default apps to install and enable nextcloud_apps_to_install: diff --git a/roles/nextcloud/templates/docker-compose.yml.j2 b/roles/nextcloud/templates/docker-compose.yml.j2 index fc86bdd..9f15760 100644 --- a/roles/nextcloud/templates/docker-compose.yml.j2 +++ b/roles/nextcloud/templates/docker-compose.yml.j2 @@ -82,8 +82,8 @@ services: POSTGRES_DB: {{ nextcloud_postgres_db }} POSTGRES_USER: {{ nextcloud_postgres_user }} POSTGRES_PASSWORD: {{ nextcloud_postgres_password }} - NEXTCLOUD_ADMIN_USER: {{ nextcloud_admin_user }} - NEXTCLOUD_ADMIN_PASSWORD: {{ nextcloud_admin_password }} + NEXTCLOUD_ADMIN_USER: {{ nextcloud_admin_user }} + NEXTCLOUD_ADMIN_PASSWORD: {{ nextcloud_admin_password }} REDIS_HOST: redis PHP_MEMORY_LIMIT: {{ nextcloud_memory_limit_mb }}M PHP_UPLOAD_LIMIT: {{ nextcloud_upload_limit_mb }}M @@ -117,7 +117,7 @@ services: {% if nextcloud_enable_notify_push %} notify-push: - image: icewind1991/notify_push + image: {{ nextcloud_notify_push_image }} restart: always depends_on: - redis From dbcccc090b5916deb18f9cde21a95337d78c3731 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 10 Apr 2026 11:19:10 +0200 Subject: [PATCH 24/39] feat: ability to set extra networks for opencloud needed for ldap outpost Signed-off-by: Bert-Jan Fikse --- roles/opencloud/defaults/main.yml | 1 + roles/opencloud/tasks/main.yml | 6 ++++++ roles/opencloud/templates/docker-compose.yml.j2 | 9 ++++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/roles/opencloud/defaults/main.yml b/roles/opencloud/defaults/main.yml index 137ece8..1e102bf 100644 --- a/roles/opencloud/defaults/main.yml +++ b/roles/opencloud/defaults/main.yml @@ -18,6 +18,7 @@ opencloud_port: 9200 opencloud_admin_password: "admin" opencloud_log_level: "warn" opencloud_extra_hosts: [] +opencloud_extra_networks: [] # Traefik configuration opencloud_traefik_network: "proxy" diff --git a/roles/opencloud/tasks/main.yml b/roles/opencloud/tasks/main.yml index 9de9625..358abb7 100644 --- a/roles/opencloud/tasks/main.yml +++ b/roles/opencloud/tasks/main.yml @@ -69,6 +69,12 @@ when: opencloud_drawio_url | length > 0 notify: restart opencloud +- name: Ensure extra networks exist + community.docker.docker_network: + name: "{{ item }}" + state: present + loop: "{{ opencloud_extra_networks }}" + - name: Create docker-compose file for opencloud template: src: docker-compose.yml.j2 diff --git a/roles/opencloud/templates/docker-compose.yml.j2 b/roles/opencloud/templates/docker-compose.yml.j2 index 10d8d22..7fafb2f 100644 --- a/roles/opencloud/templates/docker-compose.yml.j2 +++ b/roles/opencloud/templates/docker-compose.yml.j2 @@ -103,6 +103,9 @@ services: {% endif %} networks: - {{ opencloud_traefik_network }} + {% for net in opencloud_extra_networks %} + - {{ net }} +{% endfor %} {% if opencloud_extra_hosts is defined and opencloud_extra_hosts | length > 0 %} extra_hosts: {% for host in opencloud_extra_hosts %} @@ -135,4 +138,8 @@ services: networks: {{ opencloud_traefik_network }}: - external: true \ No newline at end of file + external: true +{% for net in opencloud_extra_networks %} + {{ net }}: + external: true +{% endfor %} \ No newline at end of file From d25f1c53042ea827c889e7380d576d81ff9c1e2a Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 10 Apr 2026 11:20:31 +0200 Subject: [PATCH 25/39] chore: add authentik outpost deployment Signed-off-by: Bert-Jan Fikse --- .../authentik_outpost_ldap/defaults/main.yml | 23 +++++++++++++++ roles/authentik_outpost_ldap/tasks/main.yml | 29 +++++++++++++++++++ .../templates/docker-compose.yml.j2 | 27 +++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 roles/authentik_outpost_ldap/templates/docker-compose.yml.j2 diff --git a/roles/authentik_outpost_ldap/defaults/main.yml b/roles/authentik_outpost_ldap/defaults/main.yml index 0222b44..8942bf2 100644 --- a/roles/authentik_outpost_ldap/defaults/main.yml +++ b/roles/authentik_outpost_ldap/defaults/main.yml @@ -1,3 +1,26 @@ #SPDX-License-Identifier: MIT-0 --- # defaults file for authentik_outpost_ldap + +# Base directory configuration (inherited from base role or defined here) +docker_compose_base_dir: /etc/docker/compose +docker_volume_base_dir: /srv/data + +# Service configuration +authentik_outpost_ldap_service_name: authentik-outpost-ldap +authentik_outpost_ldap_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ authentik_outpost_ldap_service_name }}" + +# Container image (must match authentik server version) +authentik_outpost_ldap_image: "ghcr.io/goauthentik/ldap:2026.2.2" + +# Connection to authentik server +authentik_outpost_ldap_host: "https://authentik.local.test" +authentik_outpost_ldap_token: "changeme" +authentik_outpost_ldap_insecure: "true" + +# Dedicated network for LDAP clients (nextcloud, opencloud, etc.) +authentik_outpost_ldap_network: "ldap" + +# Extra hosts for DNS resolution within the container +authentik_outpost_ldap_extra_hosts: [] +# - "authentik.local.test:192.168.56.11" diff --git a/roles/authentik_outpost_ldap/tasks/main.yml b/roles/authentik_outpost_ldap/tasks/main.yml index 36d90a4..7d58beb 100644 --- a/roles/authentik_outpost_ldap/tasks/main.yml +++ b/roles/authentik_outpost_ldap/tasks/main.yml @@ -1,3 +1,32 @@ #SPDX-License-Identifier: MIT-0 --- # tasks file for authentik_outpost_ldap + +- name: Create LDAP network + community.docker.docker_network: + name: "{{ authentik_outpost_ldap_network }}" + state: present + +- name: Create docker compose directory + file: + path: "{{ authentik_outpost_ldap_docker_compose_dir }}" + state: directory + mode: '0755' + +- name: Create docker-compose file for authentik LDAP outpost + template: + src: docker-compose.yml.j2 + dest: "{{ authentik_outpost_ldap_docker_compose_dir }}/docker-compose.yml" + mode: '0644' + +- name: Start authentik LDAP outpost container + community.docker.docker_compose_v2: + project_src: "{{ authentik_outpost_ldap_docker_compose_dir }}" + state: present + recreate: always + wait: true + wait_timeout: 120 + retries: 3 + delay: 15 + register: result + until: result is not failed diff --git a/roles/authentik_outpost_ldap/templates/docker-compose.yml.j2 b/roles/authentik_outpost_ldap/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..fcff9fc --- /dev/null +++ b/roles/authentik_outpost_ldap/templates/docker-compose.yml.j2 @@ -0,0 +1,27 @@ +services: + ldap: + image: {{ authentik_outpost_ldap_image }} + restart: unless-stopped + environment: + AUTHENTIK_HOST: {{ authentik_outpost_ldap_host }} + AUTHENTIK_TOKEN: {{ authentik_outpost_ldap_token }} + AUTHENTIK_INSECURE: "{{ authentik_outpost_ldap_insecure }}" +{% if authentik_outpost_ldap_extra_hosts | length > 0 %} + extra_hosts: +{% for host in authentik_outpost_ldap_extra_hosts %} + - "{{ host }}" +{% endfor %} +{% endif %} + networks: + - {{ authentik_outpost_ldap_network }} +{% if authentik_outpost_ldap_authentik_network is defined %} + - {{ authentik_outpost_ldap_authentik_network }} +{% endif %} + +networks: + {{ authentik_outpost_ldap_network }}: + external: true +{% if authentik_outpost_ldap_authentik_network is defined %} + {{ authentik_outpost_ldap_authentik_network }}: + external: true +{% endif %} From c27b4d94885271d3fcd5345273a9cdb19b0b9582 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 10 Apr 2026 13:50:32 +0200 Subject: [PATCH 26/39] feat: add blueprints for authentik ldap outpost and render values directly instead of using env vars Signed-off-by: Bert-Jan Fikse --- roles/authentik/defaults/main.yml | 46 +++++-- roles/authentik/tasks/blueprints.yml | 40 ++++-- roles/authentik/tasks/main.yml | 88 +++++++------ .../blueprints/blueprint-ldap-app.yaml.j2 | 124 ++++++++++++++++++ .../blueprints/blueprint-local-users.yaml.j2 | 21 ++- .../blueprints/blueprint-oidc-app.yaml.j2 | 8 +- .../blueprints/blueprint-source-entra.yaml.j2 | 14 +- .../templates/blueprints/outpost-ldap.yaml.j2 | 27 ++++ .../authentik/templates/docker-compose.yml.j2 | 10 -- .../templates/set-outpost-token.py.j2 | 10 ++ .../templates/wait-for-blueprints.py.j2 | 20 +++ roles/authentik_outpost_ldap/tasks/main.yml | 1 - 12 files changed, 323 insertions(+), 86 deletions(-) create mode 100644 roles/authentik/templates/blueprints/blueprint-ldap-app.yaml.j2 create mode 100644 roles/authentik/templates/blueprints/outpost-ldap.yaml.j2 create mode 100644 roles/authentik/templates/set-outpost-token.py.j2 create mode 100644 roles/authentik/templates/wait-for-blueprints.py.j2 diff --git a/roles/authentik/defaults/main.yml b/roles/authentik/defaults/main.yml index 460ba2d..9b2ca9a 100644 --- a/roles/authentik/defaults/main.yml +++ b/roles/authentik/defaults/main.yml @@ -13,7 +13,7 @@ authentik_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ authentik_service_ # Authentik service configuration authentik_domain: "authentik.local.test" -authentik_image: "ghcr.io/goauthentik/server:2025.12.0" +authentik_image: "ghcr.io/goauthentik/server:2026.2.2" authentik_port: 9000 authentik_secret_key: "changeme-generate-a-random-string" @@ -57,11 +57,29 @@ authentik_proxy_outposts: [] # authentik_host_browser: "https://authentik.local.test/" # log_level: "info" +authentik_ldap_apps: [] +# - slug: ldap +# name: LDAP +# base_dn: "dc=local,dc=test" +# search_mode: cached # cached | direct +# bind_mode: cached # cached | direct +# search_group: null # optional: group name whose members can search +# certificate: null # optional: certificate name for LDAPS +# uid_start_number: 2000 +# gid_start_number: 4000 + +authentik_ldap_outpost: {} +# name: "ldap-outpost" +# token: "changeme" # known token for outpost authentication +# config: +# authentik_host: "https://authentik.local.test/" +# log_level: "info" + authentik_oidc_apps: [] # - slug: grafana # name: Grafana -# client_id_env: GRAFANA_OIDC_CLIENT_ID -# client_secret_env: GRAFANA_OIDC_CLIENT_SECRET +# client_id: "grafana" +# client_secret: "changeme" # redirect_uris: # - url: "https://grafana.example.com/login/generic_oauth" # matching_mode: strict @@ -71,21 +89,14 @@ authentik_oidc_apps: [] # invalidation_slug: default-provider-invalidation-flow # scopes: [openid, email, profile, offline_access] -authentik_blueprint_env: [] -# GRAFANA_OIDC_CLIENT_ID: "grafana" -# GRAFANA_OIDC_CLIENT_SECRET: "{{ vault_grafana_oidc_secret }}" -# ENTRA_TENANT_ID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -# ENTRA_CLIENT_ID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -# ENTRA_CLIENT_SECRET: "{{ vault_entra_client_secret }}" - # Oauth sources authentik_entra_sources: [] # - slug: entra-id # name: "Login with Entra" # tenant_mode: single # single | common -# tenant_id_env: ENTRA_TENANT_ID -# client_id_env: ENTRA_CLIENT_ID -# client_secret_env: ENTRA_CLIENT_SECRET +# tenant_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +# client_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +# client_secret: "changeme" # scopes: # - openid # - profile @@ -105,12 +116,19 @@ authentik_login_user_fields: - username - email +# Groups to provision +authentik_groups: [] +# - name: admins +# - name: editors +# is_superuser: false +# parent: null + # Local users to provision authentik_local_users: [] # - username: admin # name: "Admin User" # email: "admin@example.com" -# password_env: AUTHENTIK_ADMIN_PASSWORD # reference env var in authentik_blueprint_env +# password: "changeme" # is_active: true # groups: # - authentik Admins diff --git a/roles/authentik/tasks/blueprints.yml b/roles/authentik/tasks/blueprints.yml index e40b774..18ef7f5 100644 --- a/roles/authentik/tasks/blueprints.yml +++ b/roles/authentik/tasks/blueprints.yml @@ -9,17 +9,17 @@ register: existing_blueprints - name: Build list of expected blueprint files + vars: + _oidc: "{{ authentik_oidc_apps | map(attribute='slug') | map('regex_replace', '^', '50-oidc-') | map('regex_replace', '$', '.yaml') | list }}" + _ldap: "{{ authentik_ldap_apps | map(attribute='slug') | map('regex_replace', '^', '55-ldap-') | map('regex_replace', '$', '.yaml') | list }}" + _proxy: "{{ authentik_proxy_apps | map(attribute='slug') | map('regex_replace', '^', '60-proxy-') | map('regex_replace', '$', '.yaml') | list }}" + _outpost: "{{ authentik_proxy_outposts | map(attribute='name') | map('regex_replace', '^', '70-outpost-') | map('regex_replace', '$', '.yaml') | list }}" + _entra: "{{ authentik_entra_sources | map(attribute='slug') | map('regex_replace', '^', '40-source-entra-') | map('regex_replace', '$', '.yaml') | list }}" + _ldap_out: "{{ ['75-outpost-ldap.yaml'] if authentik_ldap_outpost.name is defined else [] }}" + _users: "{{ ['10-local-users.yaml'] if (authentik_local_users | length > 0 or authentik_groups | length > 0) else [] }}" + _cleanup: "{{ ['00-cleanup.yaml'] if (authentik_removed_oidc_apps + authentik_removed_proxy_apps + authentik_removed_local_users) | length > 0 else [] }}" set_fact: - expected_blueprints: >- - {{ - (authentik_oidc_apps | map(attribute='slug') | map('regex_replace', '^(.*)$', '50-oidc-\1.yaml') | list) + - (authentik_proxy_apps | map(attribute='slug') | map('regex_replace', '^(.*)$', '60-proxy-\1.yaml') | list) + - (authentik_proxy_outposts | map(attribute='name') | map('regex_replace', '^(.*)$', '70-outpost-\1.yaml') | list) + - (authentik_entra_sources | map(attribute='slug') | map('regex_replace', '^(.*)$', '40-source-entra-\1.yaml') | list) + - ['45-login-sources.yaml'] + - ((authentik_local_users | length > 0) | ternary(['10-local-users.yaml'], [])) + - (((authentik_removed_oidc_apps | length > 0) or (authentik_removed_proxy_apps | length > 0) or (authentik_removed_local_users | length > 0)) | ternary(['00-cleanup.yaml'], [])) - }} + expected_blueprints: "{{ _oidc + _ldap + _proxy + _outpost + _entra + ['45-login-sources.yaml'] + _ldap_out + _users + _cleanup }}" - name: Remove stale blueprint files file: @@ -36,6 +36,14 @@ loop: "{{ authentik_oidc_apps }}" register: oidc_templates +- name: Render LDAP blueprints + ansible.builtin.template: + src: blueprints/blueprint-ldap-app.yaml.j2 + dest: "{{ authentik_docker_volume_dir }}/blueprints/55-ldap-{{ item.slug }}.yaml" + mode: "0644" + loop: "{{ authentik_ldap_apps }}" + register: ldap_templates + - name: Render Proxy blueprints ansible.builtin.template: src: blueprints/blueprint-proxy-app.yaml.j2 @@ -52,6 +60,14 @@ loop: "{{ authentik_proxy_outposts }}" register: outpost_bp +- name: Render LDAP outpost blueprint + ansible.builtin.template: + src: blueprints/outpost-ldap.yaml.j2 + dest: "{{ authentik_docker_volume_dir }}/blueprints/75-outpost-ldap.yaml" + mode: "0644" + when: authentik_ldap_outpost.name is defined + register: ldap_outpost_bp + - name: Render Entra source blueprints ansible.builtin.template: src: blueprints/blueprint-source-entra.yaml.j2 @@ -72,7 +88,7 @@ src: blueprints/blueprint-local-users.yaml.j2 dest: "{{ authentik_docker_volume_dir }}/blueprints/10-local-users.yaml" mode: "0644" - when: authentik_local_users | length > 0 + when: authentik_local_users | length > 0 or authentik_groups | length > 0 register: local_users_bp - name: Render cleanup blueprint @@ -88,8 +104,10 @@ blueprints_changed: >- {{ (oidc_templates is defined and (oidc_templates.results | selectattr('changed') | list | length > 0)) + or (ldap_templates is defined and (ldap_templates.results | selectattr('changed') | list | length > 0)) or (proxy_templates is defined and (proxy_templates.results | selectattr('changed') | list | length > 0)) or (outpost_bp is defined and (outpost_bp.results | selectattr('changed') | list | length > 0)) + or (ldap_outpost_bp.changed | default(false)) or (entra_bp is defined and (entra_bp.results | selectattr('changed') | list | length > 0)) or (login_bp is defined and login_bp.changed) or (local_users_bp.changed | default(false)) diff --git a/roles/authentik/tasks/main.yml b/roles/authentik/tasks/main.yml index aa14bd3..1471836 100644 --- a/roles/authentik/tasks/main.yml +++ b/roles/authentik/tasks/main.yml @@ -2,44 +2,18 @@ --- # tasks file for authentik -- name: Create docker compose directory +- name: Create authentik directories file: - path: "{{ authentik_docker_compose_dir }}" + path: "{{ item }}" state: directory mode: '0755' - -- name: Create authentik data directory - file: - path: "{{ authentik_docker_volume_dir }}/data" - state: directory - mode: '0755' - -- name: Create authentik certs directory - file: - path: "{{ authentik_docker_volume_dir }}/certs" - state: directory - mode: '0755' - -- name: Create authentik templates directory - file: - path: "{{ authentik_docker_volume_dir }}/templates" - state: directory - mode: '0755' - -- name: Create postgres data directory - file: - path: "{{ authentik_docker_volume_dir }}/postgresql" - state: directory - mode: '0755' - -- name: Create blueprints directory - file: - path: "{{ authentik_docker_volume_dir }}/blueprints" - state: directory - mode: '0755' - -- name: Render blueprints - import_tasks: blueprints.yml + loop: + - "{{ authentik_docker_compose_dir }}" + - "{{ authentik_docker_volume_dir }}/data" + - "{{ authentik_docker_volume_dir }}/certs" + - "{{ authentik_docker_volume_dir }}/templates" + - "{{ authentik_docker_volume_dir }}/postgresql" + - "{{ authentik_docker_volume_dir }}/blueprints" - name: Create docker-compose file for authentik template: @@ -51,6 +25,46 @@ community.docker.docker_compose_v2: project_src: "{{ authentik_docker_compose_dir }}" state: present - recreate: "{{ blueprints_changed | ternary('always', 'auto') }}" wait: true - wait_timeout: 300 \ No newline at end of file + wait_timeout: 300 + +- name: Render blueprints + import_tasks: blueprints.yml + +- name: Render blueprint wait script + template: + src: wait-for-blueprints.py.j2 + dest: "{{ authentik_docker_volume_dir }}/data/wait-for-blueprints.py" + mode: '0644' + +- name: Wait for custom blueprints to be applied + community.docker.docker_compose_v2_exec: + project_src: "{{ authentik_docker_compose_dir }}" + service: server + command: ak shell -c "exec(open('/data/wait-for-blueprints.py').read())" + register: blueprint_wait_result + changed_when: "'changed' in blueprint_wait_result.stdout" + retries: 30 + delay: 10 + until: blueprint_wait_result.rc == 0 + when: blueprints_changed + +- name: Render LDAP outpost token script + template: + src: set-outpost-token.py.j2 + dest: "{{ authentik_docker_volume_dir }}/data/set-outpost-token.py" + mode: '0644' + when: authentik_ldap_outpost.name is defined + register: ldap_token_script + +- name: Set known token for LDAP outpost + community.docker.docker_compose_v2_exec: + project_src: "{{ authentik_docker_compose_dir }}" + service: server + command: ak shell -c "exec(open('/data/set-outpost-token.py').read())" + register: ldap_token_result + changed_when: "'changed' in ldap_token_result.stdout" + retries: 30 + delay: 10 + until: ldap_token_result.rc == 0 + when: authentik_ldap_outpost.name is defined and (blueprints_changed or ldap_token_script.changed) \ No newline at end of file diff --git a/roles/authentik/templates/blueprints/blueprint-ldap-app.yaml.j2 b/roles/authentik/templates/blueprints/blueprint-ldap-app.yaml.j2 new file mode 100644 index 0000000..6747d95 --- /dev/null +++ b/roles/authentik/templates/blueprints/blueprint-ldap-app.yaml.j2 @@ -0,0 +1,124 @@ +# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json +version: 1 +metadata: + name: "ldap-{{ item.slug }}" + labels: + blueprints.goauthentik.io/instantiate: "true" + blueprints.goauthentik.io/description: "LDAP provider + application for {{ item.slug }}" + +entries: + # Simple password-only flow for LDAP bind (no browser policies) + - model: authentik_stages_password.passwordstage + id: ldap-password-stage + identifiers: + name: ldap-bind-password + attrs: + name: ldap-bind-password + backends: + - authentik.core.auth.InbuiltBackend + - authentik.core.auth.TokenBackend + - authentik.sources.ldap.auth.LDAPBackend + + - model: authentik_stages_identification.identificationstage + id: ldap-identification-stage + identifiers: + name: ldap-bind-identification + attrs: + name: ldap-bind-identification + user_fields: + - username + - email + password_stage: !KeyOf ldap-password-stage + + - model: authentik_stages_user_login.userloginstage + id: ldap-login-stage + identifiers: + name: ldap-bind-login + attrs: + name: ldap-bind-login + + - model: authentik_flows.flow + id: ldap-bind-flow + identifiers: + slug: ldap-bind + attrs: + name: LDAP Bind + slug: ldap-bind + title: LDAP Bind + designation: authentication + authentication: none + + - model: authentik_flows.flowstagebinding + identifiers: + target: !KeyOf ldap-bind-flow + stage: !KeyOf ldap-identification-stage + order: 0 + attrs: + target: !KeyOf ldap-bind-flow + stage: !KeyOf ldap-identification-stage + order: 0 + + - model: authentik_flows.flowstagebinding + identifiers: + target: !KeyOf ldap-bind-flow + stage: !KeyOf ldap-login-stage + order: 10 + attrs: + target: !KeyOf ldap-bind-flow + stage: !KeyOf ldap-login-stage + order: 10 + +{% if item.search_group is defined and item.search_group %} + - model: authentik_rbac.role + id: ldap-search-role-{{ item.slug }} + identifiers: + name: ldap-search-{{ item.slug }} + attrs: + name: ldap-search-{{ item.slug }} +{% endif %} + + - model: authentik_providers_ldap.ldapprovider + id: ldap-provider-{{ item.slug }} + identifiers: + name: {{ item.name }} + attrs: + name: {{ item.name }} + base_dn: "{{ item.base_dn }}" + authorization_flow: !KeyOf ldap-bind-flow + invalidation_flow: !Find [authentik_flows.flow, [slug, {{ item.invalidation_flow_slug | default('default-provider-invalidation-flow') }}]] + authentication_flow: !KeyOf ldap-bind-flow + search_mode: {{ item.search_mode | default('cached') }} + bind_mode: {{ item.bind_mode | default('direct') }} +{% if item.certificate is defined and item.certificate %} + certificate: !Find [authentik_crypto.certificatekeypair, [name, {{ item.certificate }}]] +{% endif %} +{% if item.uid_start_number is defined %} + uid_start_number: {{ item.uid_start_number }} +{% endif %} +{% if item.gid_start_number is defined %} + gid_start_number: {{ item.gid_start_number }} +{% endif %} +{% if item.search_group is defined and item.search_group %} + permissions: + - permission: authentik_providers_ldap.search_full_directory + role: !KeyOf ldap-search-role-{{ item.slug }} +{% endif %} + +{% if item.search_group is defined and item.search_group %} + # Assign the LDAP search role to the search group + - model: authentik_core.group + identifiers: + name: {{ item.search_group }} + attrs: + roles: + - !KeyOf ldap-search-role-{{ item.slug }} +{% endif %} + + - model: authentik_core.application + id: app-{{ item.slug }} + identifiers: + slug: {{ item.slug }} + attrs: + name: "{{ item.name | default(item.slug) }}" + slug: {{ item.slug }} + provider: !KeyOf ldap-provider-{{ item.slug }} diff --git a/roles/authentik/templates/blueprints/blueprint-local-users.yaml.j2 b/roles/authentik/templates/blueprints/blueprint-local-users.yaml.j2 index d40454b..8a158b0 100644 --- a/roles/authentik/templates/blueprints/blueprint-local-users.yaml.j2 +++ b/roles/authentik/templates/blueprints/blueprint-local-users.yaml.j2 @@ -4,9 +4,24 @@ metadata: name: "local-users" labels: blueprints.goauthentik.io/instantiate: "true" - blueprints.goauthentik.io/description: "Local user accounts" + blueprints.goauthentik.io/description: "Local groups and user accounts" entries: +{% for group in authentik_groups %} + - model: authentik_core.group + id: group-{{ group.name | regex_replace('[^a-zA-Z0-9]', '-') }} + identifiers: + name: {{ group.name }} + attrs: + name: {{ group.name }} +{% if group.is_superuser is defined %} + is_superuser: {{ group.is_superuser | lower }} +{% endif %} +{% if group.parent is defined and group.parent %} + parent: !Find [authentik_core.group, [name, {{ group.parent }}]] +{% endif %} + +{% endfor %} {% for user in authentik_local_users %} - model: authentik_core.user id: user-{{ user.username }} @@ -17,8 +32,8 @@ entries: name: "{{ user.name | default(user.username) }}" email: "{{ user.email | default('') }}" is_active: {{ user.is_active | default(true) | lower }} -{% if user.password_env is defined %} - password: !Env {{ user.password_env }} +{% if user.password is defined %} + password: "{{ user.password }}" {% endif %} {% if user.groups is defined and user.groups | length > 0 %} groups: diff --git a/roles/authentik/templates/blueprints/blueprint-oidc-app.yaml.j2 b/roles/authentik/templates/blueprints/blueprint-oidc-app.yaml.j2 index 7270de8..b5813a8 100644 --- a/roles/authentik/templates/blueprints/blueprint-oidc-app.yaml.j2 +++ b/roles/authentik/templates/blueprints/blueprint-oidc-app.yaml.j2 @@ -13,9 +13,11 @@ entries: name: {{ item.slug }} attrs: name: {{ item.slug }} - client_type: confidential - client_id: !Env {{ item.client_id_env }} - client_secret: !Env {{ item.client_secret_env }} + client_type: {{ item.client_type | default('confidential') }} + client_id: "{{ item.client_id }}" +{% if item.client_type | default('confidential') == 'confidential' %} + client_secret: "{{ item.client_secret }}" +{% endif %} redirect_uris: {% for ru in item.redirect_uris %} diff --git a/roles/authentik/templates/blueprints/blueprint-source-entra.yaml.j2 b/roles/authentik/templates/blueprints/blueprint-source-entra.yaml.j2 index acab07b..00bf902 100644 --- a/roles/authentik/templates/blueprints/blueprint-source-entra.yaml.j2 +++ b/roles/authentik/templates/blueprints/blueprint-source-entra.yaml.j2 @@ -15,12 +15,12 @@ entries: name: "{{ item.name | default('Microsoft Entra ID') }}" slug: {{ item.slug }} - # Authentik’s OAuth sources support vendor-specific types. - # Entra guide calls it “Entra ID OAuth Source”. + # Authentik's OAuth sources support vendor-specific types. + # Entra guide calls it "Entra ID OAuth Source". provider_type: entraid - consumer_key: !Env {{ item.client_id_env }} - consumer_secret: !Env {{ item.client_secret_env }} + consumer_key: "{{ item.client_id }}" + consumer_secret: "{{ item.client_secret }}" scopes: {% for s in (item.scopes | default(['openid','profile','email'])) %} @@ -28,10 +28,10 @@ entries: {% endfor %} {% if (item.tenant_mode | default('single')) == 'single' %} - authorization_url: !Format ["https://login.microsoftonline.com/%s/oauth2/v2.0/authorize", !Env {{ item.tenant_id_env }}] - access_token_url: !Format ["https://login.microsoftonline.com/%s/oauth2/v2.0/token", !Env {{ item.tenant_id_env }}] + authorization_url: "https://login.microsoftonline.com/{{ item.tenant_id }}/oauth2/v2.0/authorize" + access_token_url: "https://login.microsoftonline.com/{{ item.tenant_id }}/oauth2/v2.0/token" profile_url: "https://graph.microsoft.com/v1.0/me" - oidc_jwks_url: !Format ["https://login.microsoftonline.com/%s/discovery/v2.0/keys", !Env {{ item.tenant_id_env }}] + oidc_jwks_url: "https://login.microsoftonline.com/{{ item.tenant_id }}/discovery/v2.0/keys" {% else %} authorization_url: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" access_token_url: "https://login.microsoftonline.com/common/oauth2/v2.0/token" diff --git a/roles/authentik/templates/blueprints/outpost-ldap.yaml.j2 b/roles/authentik/templates/blueprints/outpost-ldap.yaml.j2 new file mode 100644 index 0000000..e1d5fbf --- /dev/null +++ b/roles/authentik/templates/blueprints/outpost-ldap.yaml.j2 @@ -0,0 +1,27 @@ +# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json +version: 1 +metadata: + name: "outpost-{{ authentik_ldap_outpost.name }}" + labels: + blueprints.goauthentik.io/instantiate: "true" + +entries: + - model: authentik_outposts.outpost + identifiers: + name: "{{ authentik_ldap_outpost.name }}" + attrs: + name: "{{ authentik_ldap_outpost.name }}" + type: ldap + service_connection: null + + providers: +{% for app in authentik_ldap_apps %} + - !Find [authentik_providers_ldap.ldapprovider, [name, {{ app.name }}]] +{% endfor %} + +{% if authentik_ldap_outpost.config is defined %} + config: +{% for k, v in authentik_ldap_outpost.config.items() %} + {{ k }}: {{ v | tojson }} +{% endfor %} +{% endif %} diff --git a/roles/authentik/templates/docker-compose.yml.j2 b/roles/authentik/templates/docker-compose.yml.j2 index 6daa2a1..c9796a2 100644 --- a/roles/authentik/templates/docker-compose.yml.j2 +++ b/roles/authentik/templates/docker-compose.yml.j2 @@ -35,11 +35,6 @@ services: AUTHENTIK_POSTGRESQL__PASSWORD: {{ authentik_postgres_password }} AUTHENTIK_LOG_LEVEL: {{ authentik_log_level }} AUTHENTIK_ERROR_REPORTING__ENABLED: "{{ authentik_error_reporting_enabled | lower }}" -{% if authentik_blueprint_env|length > 0 %} -{% for k, v in authentik_blueprint_env.items() %} - {{ k }}: "{{ v }}" -{% endfor %} -{% endif %} volumes: - {{ authentik_docker_volume_dir }}/blueprints:/blueprints/custom - {{ authentik_docker_volume_dir }}/data:/data @@ -75,11 +70,6 @@ services: AUTHENTIK_POSTGRESQL__PASSWORD: {{ authentik_postgres_password }} AUTHENTIK_LOG_LEVEL: {{ authentik_log_level }} AUTHENTIK_ERROR_REPORTING__ENABLED: "{{ authentik_error_reporting_enabled | lower }}" -{% if authentik_blueprint_env|length > 0 %} -{% for k, v in authentik_blueprint_env.items() %} - {{ k }}: "{{ v }}" -{% endfor %} -{% endif %} volumes: - {{ authentik_docker_volume_dir }}/data:/data - {{ authentik_docker_volume_dir }}/certs:/certs diff --git a/roles/authentik/templates/set-outpost-token.py.j2 b/roles/authentik/templates/set-outpost-token.py.j2 new file mode 100644 index 0000000..0b61705 --- /dev/null +++ b/roles/authentik/templates/set-outpost-token.py.j2 @@ -0,0 +1,10 @@ +from authentik.outposts.models import Outpost +from authentik.core.models import Token +o = Outpost.objects.get(name='{{ authentik_ldap_outpost.name }}') +t = Token.objects.get(identifier=o.token_identifier) +if t.key != '{{ authentik_ldap_outpost.token }}': + t.key = '{{ authentik_ldap_outpost.token }}' + t.save(update_fields=['key']) + print('changed') +else: + print('ok') diff --git a/roles/authentik/templates/wait-for-blueprints.py.j2 b/roles/authentik/templates/wait-for-blueprints.py.j2 new file mode 100644 index 0000000..ff78f24 --- /dev/null +++ b/roles/authentik/templates/wait-for-blueprints.py.j2 @@ -0,0 +1,20 @@ +from authentik.blueprints.models import BlueprintInstance +from authentik.blueprints.v1.importer import Importer + +failed = list(BlueprintInstance.objects.filter(enabled=True, path__startswith="custom/").exclude(status="successful").order_by("path")) +if not failed: + print("ok") +else: + for bp in failed: + content = bp.retrieve() + importer = Importer.from_string(content) + valid, _ = importer.validate() + if valid: + importer.apply() + bp.status = "successful" + bp.save() + still_failed = BlueprintInstance.objects.filter(enabled=True, path__startswith="custom/").exclude(status="successful") + if still_failed.exists(): + names = ", ".join(bp.name for bp in still_failed) + raise Exception(f"Blueprints still failing: {names}") + print("changed") diff --git a/roles/authentik_outpost_ldap/tasks/main.yml b/roles/authentik_outpost_ldap/tasks/main.yml index 7d58beb..79a350a 100644 --- a/roles/authentik_outpost_ldap/tasks/main.yml +++ b/roles/authentik_outpost_ldap/tasks/main.yml @@ -23,7 +23,6 @@ community.docker.docker_compose_v2: project_src: "{{ authentik_outpost_ldap_docker_compose_dir }}" state: present - recreate: always wait: true wait_timeout: 120 retries: 3 From 967ffb0c2d17b014ea71a74c589ea9612c27f454 Mon Sep 17 00:00:00 2001 From: Bert-Jan Fikse Date: Fri, 10 Apr 2026 14:34:15 +0200 Subject: [PATCH 27/39] fix: leading space in extra networks Signed-off-by: Bert-Jan Fikse --- roles/opencloud/templates/docker-compose.yml.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/opencloud/templates/docker-compose.yml.j2 b/roles/opencloud/templates/docker-compose.yml.j2 index 7fafb2f..b731526 100644 --- a/roles/opencloud/templates/docker-compose.yml.j2 +++ b/roles/opencloud/templates/docker-compose.yml.j2 @@ -103,7 +103,7 @@ services: {% endif %} networks: - {{ opencloud_traefik_network }} - {% for net in opencloud_extra_networks %} +{% for net in opencloud_extra_networks %} - {{ net }} {% endfor %} {% if opencloud_extra_hosts is defined and opencloud_extra_hosts | length > 0 %} From 1fcb433aae55e2dcf3a6093e50b09102c378a0ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Thu, 15 Jan 2026 16:31:27 +0100 Subject: [PATCH 28/39] chore: add new boilerplate role for homarr --- roles/homarr/README.md | 38 ++++++++++++++++++++++++++++++++++ roles/homarr/defaults/main.yml | 3 +++ roles/homarr/handlers/main.yml | 3 +++ roles/homarr/meta/main.yml | 35 +++++++++++++++++++++++++++++++ roles/homarr/tasks/main.yml | 3 +++ roles/homarr/tests/inventory | 2 ++ roles/homarr/tests/test.yml | 6 ++++++ roles/homarr/vars/main.yml | 3 +++ 8 files changed, 93 insertions(+) create mode 100644 roles/homarr/README.md create mode 100644 roles/homarr/defaults/main.yml create mode 100644 roles/homarr/handlers/main.yml create mode 100644 roles/homarr/meta/main.yml create mode 100644 roles/homarr/tasks/main.yml create mode 100644 roles/homarr/tests/inventory create mode 100644 roles/homarr/tests/test.yml create mode 100644 roles/homarr/vars/main.yml diff --git a/roles/homarr/README.md b/roles/homarr/README.md new file mode 100644 index 0000000..da76bcd --- /dev/null +++ b/roles/homarr/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). \ No newline at end of file diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml new file mode 100644 index 0000000..bc30dcc --- /dev/null +++ b/roles/homarr/defaults/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# defaults file for homarr \ No newline at end of file diff --git a/roles/homarr/handlers/main.yml b/roles/homarr/handlers/main.yml new file mode 100644 index 0000000..56f5283 --- /dev/null +++ b/roles/homarr/handlers/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# handlers file for homarr \ No newline at end of file diff --git a/roles/homarr/meta/main.yml b/roles/homarr/meta/main.yml new file mode 100644 index 0000000..faea947 --- /dev/null +++ b/roles/homarr/meta/main.yml @@ -0,0 +1,35 @@ +#SPDX-License-Identifier: MIT-0 +galaxy_info: + author: your name + description: your role description + company: your company (optional) + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.2 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. \ No newline at end of file diff --git a/roles/homarr/tasks/main.yml b/roles/homarr/tasks/main.yml new file mode 100644 index 0000000..c2dc205 --- /dev/null +++ b/roles/homarr/tasks/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# tasks file for homarr \ No newline at end of file diff --git a/roles/homarr/tests/inventory b/roles/homarr/tests/inventory new file mode 100644 index 0000000..712db59 --- /dev/null +++ b/roles/homarr/tests/inventory @@ -0,0 +1,2 @@ +#SPDX-License-Identifier: MIT-0 +localhost diff --git a/roles/homarr/tests/test.yml b/roles/homarr/tests/test.yml new file mode 100644 index 0000000..88ecfc1 --- /dev/null +++ b/roles/homarr/tests/test.yml @@ -0,0 +1,6 @@ +#SPDX-License-Identifier: MIT-0 +--- +- hosts: localhost + remote_user: root + roles: + - homarr \ No newline at end of file diff --git a/roles/homarr/vars/main.yml b/roles/homarr/vars/main.yml new file mode 100644 index 0000000..984df2b --- /dev/null +++ b/roles/homarr/vars/main.yml @@ -0,0 +1,3 @@ +#SPDX-License-Identifier: MIT-0 +--- +# vars file for homarr \ No newline at end of file From 5608daadaa49dfa2e6488db5a261407e5faa1670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Fri, 23 Jan 2026 15:45:59 +0100 Subject: [PATCH 29/39] chore: base config and deployment for role homarr --- roles/homarr/defaults/main.yml | 22 +++++++++++++- roles/homarr/tasks/main.yml | 18 +++++++++++- roles/homarr/templates/docker-compose.yml.j2 | 31 ++++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 roles/homarr/templates/docker-compose.yml.j2 diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index bc30dcc..c5dccef 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -1,3 +1,23 @@ #SPDX-License-Identifier: MIT-0 --- -# defaults file for homarr \ No newline at end of file +# defaults file for homarr + +# Base directory configuration (inherited from base role or defined here) +docker_compose_base_dir: /etc/docker/compose +docker_volume_base_dir: /srv/data + +# homarr-specific configuration +homarr_service_name: homarr +homarr_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ homarr_service_name }}" +homarr_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ homarr_service_name }}" + +# Service configuration +homarr_domain: "homarr.local.test" +homarr_image: "ghcr.io/homarr-labs/homarr:latest" +homarr_secret_encription_key: "CHANGE_ME" +homarr_port: 7575 +homarr_use_docker: false + +# Traefik configuration +homarr_traefik_network: "proxy" +homarr_use_ssl: true \ No newline at end of file diff --git a/roles/homarr/tasks/main.yml b/roles/homarr/tasks/main.yml index c2dc205..17c3bf5 100644 --- a/roles/homarr/tasks/main.yml +++ b/roles/homarr/tasks/main.yml @@ -1,3 +1,19 @@ #SPDX-License-Identifier: MIT-0 --- -# tasks file for homarr \ No newline at end of file +# tasks file for homarr +- name: Create docker compose directory + file: + path: "{{ homarr_docker_compose_dir }}" + state: directory + mode: '0755' + +- name: Create docker-compose file for homarr + template: + src: docker-compose.yml.j2 + dest: "{{ homarr_docker_compose_dir }}/docker-compose.yml" + mode: '0644' + +- name: Start homarr containers + community.docker.docker_compose_v2: + project_src: "{{ homarr_docker_compose_dir }}" + state: present \ No newline at end of file diff --git a/roles/homarr/templates/docker-compose.yml.j2 b/roles/homarr/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..7992c7c --- /dev/null +++ b/roles/homarr/templates/docker-compose.yml.j2 @@ -0,0 +1,31 @@ +#---------------------------------------------------------------------# +# Homarr - A simple, yet powerful dashboard for your server. # +#---------------------------------------------------------------------# +services: + homarr: + container_name: {{ homarr_service_name }} + image: {{ homarr_image }} + restart: unless-stopped + volumes: +{% if homarr_use_docker %} + - /var/run/docker.sock:/var/run/docker.sock # Optional, only if you want docker integration +{% endif %} + - {{ homarr_docker_volume_dir }}/homarr/appdata:/appdata + environment: + - SECRET_ENCRYPTION_KEY={{ homarr_secret_encryption_key }} + networks: + - {{ homarr_traefik_network }} + labels: + - traefik.enable=true + - traefik.docker.network={{ homarr_traefik_network }} + - traefik.http.routers.{{ homarr_service_name }}.rule=Host(`{{ homarr_domain }}`) +{% if homarr_use_ssl %} + - traefik.http.routers.{{ homarr_service_name }}.entrypoints=websecure + - traefik.http.routers.{{ homarr_service_name }}.tls=true +{% else %} + - traefik.http.routers.{{ homarr_service_name }}.entrypoints=web +{% endif %} + - traefik.http.services.{{ homarr_service_name }}.loadbalancer.server.port={{ homarr_port }} +networks: + {{ homarr_traefik_network }}: + external: true \ No newline at end of file From 23ea8dafc9b4ca40bcdef8b2bec4a53a7ab0ff3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Tue, 7 Apr 2026 16:58:28 +0200 Subject: [PATCH 30/39] Chore: add admin user and seed staging added creation of the admin user, the basic homeboard and all basic setup tasks. Todo: Cleanup --- roles/homarr/defaults/main.yml | 31 +- roles/homarr/tasks/main.yml | 383 ++++++++++++++++++- roles/homarr/templates/docker-compose.yml.j2 | 18 +- 3 files changed, 424 insertions(+), 8 deletions(-) diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index c5dccef..78b32ab 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -7,17 +7,44 @@ docker_compose_base_dir: /etc/docker/compose docker_volume_base_dir: /srv/data # homarr-specific configuration +homarr_base_path: /srv/data/homarr homarr_service_name: homarr homarr_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ homarr_service_name }}" homarr_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ homarr_service_name }}" +homarr_appdata_dir: "{{ homarr_docker_volume_dir }}/{{ homarr_service_name }}/appdata" +homarr_db_dir: "{{ homarr_appdata_dir }}/db/db.sqlite" # Service configuration homarr_domain: "homarr.local.test" homarr_image: "ghcr.io/homarr-labs/homarr:latest" -homarr_secret_encription_key: "CHANGE_ME" +homarr_secret_encryption_key: "4fc2f54f54be3f4439b728da81b743fb0ee6317fd1a24f4096611f68019fa5a7" homarr_port: 7575 homarr_use_docker: false +# URL – wird für BASE_URL, NEXTAUTH_URL und die Completion-Message verwendet +homarr_base_url: "https://home.local.test" + +# OIDC Konfiguration +oidc_issuer: "https://auth.digitalboard.ch/realms/Digitalboard" +oidc_client_id: "homarr-digitalboard" +oidc_client_name: "Digitalboard" +oidc_scopes: "openid profile email groups" +oidc_groups_attribute: "groups" +oidc_client_secret: "mein-test-secret-aus-keycloak" +oidc_auto_login: "false" + +# OIDC Admin-Gruppe (muss in Keycloak existieren) +oidc_admin_group: "homarr-admins" + +# Board Konfiguration +default_board_name: "Home" +default_board_public: true + # Traefik configuration homarr_traefik_network: "proxy" -homarr_use_ssl: true \ No newline at end of file +homarr_use_ssl: true + +# Lokaler Admin +homarr_admin_username: "admin" +homarr_admin_email: "admin@digitalboard.ch" +homarr_admin_password: "ChangeMe123!" \ No newline at end of file diff --git a/roles/homarr/tasks/main.yml b/roles/homarr/tasks/main.yml index 17c3bf5..f8dd3df 100644 --- a/roles/homarr/tasks/main.yml +++ b/roles/homarr/tasks/main.yml @@ -1,14 +1,47 @@ #SPDX-License-Identifier: MIT-0 --- # tasks file for homarr + +# ===================================================================== +# 1. VORBEREITUNG: Pakete und Verzeichnisse VOR Container-Start +# ===================================================================== + +- name: Ensure required packages are installed + ansible.builtin.package: + name: + - sqlite3 + - python3-docker + - python3-bcrypt + state: present + - name: Create docker compose directory - file: + ansible.builtin.file: path: "{{ homarr_docker_compose_dir }}" state: directory mode: '0755' +- name: Create Homarr data directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "1000" + group: "1000" + mode: "0755" + loop: + - "{{ homarr_appdata_dir }}" + - "{{ homarr_appdata_dir }}/db" + +- name: Check if database already exists + ansible.builtin.stat: + path: "{{ homarr_db_dir }}" + register: db_exists + +# ===================================================================== +# 2. CONTAINER STARTEN +# ===================================================================== + - name: Create docker-compose file for homarr - template: + ansible.builtin.template: src: docker-compose.yml.j2 dest: "{{ homarr_docker_compose_dir }}/docker-compose.yml" mode: '0644' @@ -16,4 +49,348 @@ - name: Start homarr containers community.docker.docker_compose_v2: project_src: "{{ homarr_docker_compose_dir }}" - state: present \ No newline at end of file + state: present + +# ===================================================================== +# 3. AUF DATENBANK WARTEN +# ===================================================================== + +- name: Wait for database to be created by Homarr + ansible.builtin.wait_for: + path: "{{ homarr_db_dir }}" + state: present + timeout: 60 + when: not db_exists.stat.exists + +- name: Wait for database schema to be initialized + ansible.builtin.shell: | + i=0 + while [ $i -lt 30 ]; do + if sqlite3 "{{ homarr_db_dir }}" "SELECT name FROM sqlite_master WHERE type='table' AND name='board';" 2>/dev/null | grep -q board; then + exit 0 + fi + sleep 2 + i=$((i + 1)) + done + exit 1 + register: schema_check + changed_when: false + when: not db_exists.stat.exists + +- name: Ensure python3-bcrypt is installed + ansible.builtin.package: + name: python3-bcrypt + state: present + +- name: Generate bcrypt hash for admin password + ansible.builtin.shell: | + python3 -c " + import bcrypt + password = '{{ homarr_admin_password }}'.encode() + salt = bcrypt.gensalt(rounds=10) + hashed = bcrypt.hashpw(password, salt) + print(salt.decode()) + print(hashed.decode()) + " + register: bcrypt_output + changed_when: false + no_log: true + +# ===================================================================== +# 4. DATENBANK SEEDEN (nur wenn Onboarding noch nicht abgeschlossen) +# ===================================================================== + +- name: Check if onboarding is already completed + ansible.builtin.shell: | + sqlite3 "{{ homarr_db_dir }}" "SELECT step FROM onboarding WHERE step='finish';" 2>/dev/null + register: onboarding_status + changed_when: false + failed_when: false + +- name: Seed Homarr database + ansible.builtin.shell: | + sqlite3 "{{ homarr_db_dir }}" << 'SEEDSQL' + -- SERVER SETTINGS + INSERT OR REPLACE INTO serverSetting (setting_key, value) + VALUES + ('analytics', '{"json": {"enableGeneral": false, "enableWidgetData": false, "enableIntegrationData": false}}'), + ('culture', '{"json": {"defaultLocale": "de"}}'), + ('crawling', '{"json": {"crawlingEnabled": false}}'), + ('board', '{"json": {"defaultBoardId": "board-default"}}'); + + -- ONBOARDING ÜBERSPRINGEN + UPDATE onboarding SET step = 'finish', previous_step = 'settings'; + + -- ===================================================================== + -- GRUPPEN (müssen VOR groupMember existieren) + -- ===================================================================== + + -- OIDC-ADMIN GRUPPE + INSERT OR IGNORE INTO "group" (id, name, owner_id, position) + VALUES ('group-oidc-admins', '{{ oidc_admin_group | default("homarr-admins") }}', NULL, 0); + + INSERT OR IGNORE INTO groupPermission (group_id, permission) + VALUES + ('group-oidc-admins', 'admin'), + ('group-oidc-admins', 'board-create'), + ('group-oidc-admins', 'board-full-access'), + ('group-oidc-admins', 'integration-create'), + ('group-oidc-admins', 'integration-full-access'); + + -- CREDENTIALS-ADMIN GRUPPE + INSERT OR IGNORE INTO "group" (id, name, owner_id, position) + VALUES ('group-credentials-admin', 'credentials-admin', NULL, 1); + + INSERT OR IGNORE INTO groupPermission (group_id, permission) + VALUES + ('group-credentials-admin', 'admin'), + ('group-credentials-admin', 'board-create'), + ('group-credentials-admin', 'board-full-access'), + ('group-credentials-admin', 'integration-create'), + ('group-credentials-admin', 'integration-full-access'); + + -- ===================================================================== + -- LOKALER ADMIN USER (Passwort wird via CLI gesetzt) + -- ===================================================================== + + INSERT OR IGNORE INTO user (id, name, email, password, salt, email_verified, provider) + VALUES ( + 'user-local-admin', + '{{ homarr_admin_username | default("admin") }}', + '{{ homarr_admin_email | default("admin@digitalboard.ch") }}', + '{{ bcrypt_output.stdout_lines[1] }}', + '{{ bcrypt_output.stdout_lines[0] }}', + 1, + 'credentials' + ); + + -- ADMIN-USER DEN GRUPPEN ZUWEISEN (Gruppen existieren jetzt) + + INSERT OR IGNORE INTO groupMember (group_id, user_id) + VALUES + ('group-credentials-admin', 'user-local-admin'), + ('group-oidc-admins', 'user-local-admin'); + + -- ===================================================================== + -- BOARD + -- ===================================================================== + + INSERT OR IGNORE INTO board ( + id, name, is_public, + primary_color, secondary_color, opacity, + background_image_attachment, background_image_repeat, background_image_size, + item_radius, disable_status + ) + VALUES ( + 'board-default', + '{{ default_board_name | default("Dashboard") }}', + {% if default_board_public | default(true) %}1{% else %}0{% endif %}, + '#fa5252', + '#fd7e14', + 100, + 'fixed', + 'no-repeat', + 'cover', + 'lg', + 0 + ); + + -- LAYOUTS + INSERT OR IGNORE INTO layout (id, name, board_id, column_count, breakpoint) + VALUES + ('layout-desktop', 'Desktop', 'board-default', 10, 0), + ('layout-tablet', 'Tablet', 'board-default', 6, 768), + ('layout-mobile', 'Mobile', 'board-default', 2, 480); + + -- HOME BOARD FÜR ADMIN SETZEN (Board existiert jetzt) + UPDATE user SET home_board_id = 'board-default', mobile_home_board_id = 'board-default' + WHERE id = 'user-local-admin'; + + -- SEKTION + DELETE FROM section_layout WHERE section_id = 'section-apps'; + DELETE FROM item_layout WHERE section_id = 'section-apps'; + DELETE FROM section WHERE id = 'section-apps'; + + INSERT INTO section (id, board_id, kind, x_offset, y_offset, name, options) + VALUES ( + 'section-apps', + 'board-default', + 'empty', + 0, + 0, + 'Anwendungen', + '{"json": {}}' + ); + + INSERT OR REPLACE INTO section_layout (section_id, layout_id, parent_section_id, x_offset, y_offset, width, height) + VALUES + ('section-apps', 'layout-desktop', NULL, 0, 0, 10, 3), + ('section-apps', 'layout-tablet', NULL, 0, 0, 6, 4), + ('section-apps', 'layout-mobile', NULL, 0, 0, 2, 6); + + -- BOARD-BERECHTIGUNG + INSERT OR IGNORE INTO boardGroupPermission (board_id, group_id, permission) + VALUES + ('board-default', 'group-oidc-admins', 'full-access'), + ('board-default', 'group-credentials-admin', 'full-access'); + + -- ===================================================================== + -- APPS + -- ===================================================================== + + -- Nextcloud + INSERT OR IGNORE INTO app (id, name, description, icon_url, href) + VALUES ( + 'app-nextcloud', + 'Nextcloud', + 'Cloud Storage & Collaboration', + 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png', + 'https://cloud.digitalboard.ch' + ); + + INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) + VALUES ( + 'item-nextcloud', + 'board-default', + 'app', + '{"json": {"appId": "app-nextcloud"}}', + '{"json": {}}' + ); + + INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) + VALUES + ('item-nextcloud', 'section-apps', 'layout-desktop', 0, 0, 2, 1), + ('item-nextcloud', 'section-apps', 'layout-tablet', 0, 0, 2, 1), + ('item-nextcloud', 'section-apps', 'layout-mobile', 0, 0, 1, 1); + + -- Keycloak + INSERT OR IGNORE INTO app (id, name, description, icon_url, href) + VALUES ( + 'app-keycloak', + 'Keycloak', + 'Identity & Access Management', + 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/keycloak.png', + 'https://auth.digitalboard.ch' + ); + + INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) + VALUES ( + 'item-keycloak', + 'board-default', + 'app', + '{"json": {"appId": "app-keycloak"}}', + '{"json": {}}' + ); + + INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) + VALUES + ('item-keycloak', 'section-apps', 'layout-desktop', 2, 0, 2, 1), + ('item-keycloak', 'section-apps', 'layout-tablet', 2, 0, 2, 1), + ('item-keycloak', 'section-apps', 'layout-mobile', 1, 0, 1, 1); + + -- Mailman + INSERT OR IGNORE INTO app (id, name, description, icon_url, href) + VALUES ( + 'app-mailman', + 'Mailman', + 'Mailing List Manager', + 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/mailman.png', + 'https://lists.digitalboard.ch' + ); + + INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) + VALUES ( + 'item-mailman', + 'board-default', + 'app', + '{"json": {"appId": "app-mailman"}}', + '{"json": {}}' + ); + + INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) + VALUES + ('item-mailman', 'section-apps', 'layout-desktop', 4, 0, 2, 1), + ('item-mailman', 'section-apps', 'layout-tablet', 4, 0, 2, 1), + ('item-mailman', 'section-apps', 'layout-mobile', 0, 1, 1, 1); + SEEDSQL + args: + executable: /bin/bash + register: seed_result + changed_when: seed_result.rc == 0 + when: onboarding_status.stdout is not defined or 'finish' not in onboarding_status.stdout + +# ===================================================================== +# 5. RESTART, HEALTH-CHECK, DANN CLI +# ===================================================================== + +- name: Restart Homarr to apply database changes + ansible.builtin.shell: + cmd: docker compose restart + chdir: "{{ homarr_docker_compose_dir }}" + when: seed_result is changed + +- name: Wait for Homarr to be ready + ansible.builtin.shell: + cmd: docker compose exec -T {{ homarr_service_name }} wget -qO /dev/null "http://localhost:7575" 2>&1 + chdir: "{{ homarr_docker_compose_dir }}" + retries: 30 + delay: 5 + register: homarr_ready + until: homarr_ready.rc == 0 + changed_when: false + +- name: Display completion message + ansible.builtin.debug: + msg: | + ============================================================ + Homarr deployed! + URL: {{ homarr_base_url }} + Admin-User: {{ homarr_admin_username }} + Admin-Passwort: {{ homarr_admin_password }} + (Bitte sofort ändern!) + ============================================================ + when: not db_exists.stat.exists + +# ===================================================================== +# 6. ABSCHLUSS +# ===================================================================== + +- name: Display admin credentials + ansible.builtin.debug: + msg: | + ============================================ + LOKALER ADMIN PASSWORT (bitte sofort ändern!) + {{ admin_password_output.stdout }} + ============================================ + when: + - admin_password_output is defined + - admin_password_output.stdout is defined + - admin_password_output.stdout | length > 0 + +- name: Display completion message + ansible.builtin.debug: + msg: | + ============================================================ + Homarr wurde erfolgreich deployed! + ============================================================ + + URL: {{ homarr_base_url }} + + OIDC Konfiguration: + - Issuer: {{ oidc_issuer }} + - Client ID: {{ oidc_client_id }} + - Admin-Gruppe: {{ oidc_admin_group }} + + Nächste Schritte: + 1. Stelle sicher, dass in Keycloak die Gruppe "{{ oidc_admin_group }}" existiert + 2. Füge deinen User zur Gruppe "{{ oidc_admin_group }}" hinzu + 3. Öffne {{ homarr_base_url }} + 4. Du solltest automatisch via OIDC eingeloggt werden + + Das Standard-Board "{{ default_board_name }}" wurde erstellt mit: + - Nextcloud App + - Keycloak App + - Mailman App + + Weitere Apps kannst du direkt in der Homarr UI hinzufügen. + ============================================================ \ No newline at end of file diff --git a/roles/homarr/templates/docker-compose.yml.j2 b/roles/homarr/templates/docker-compose.yml.j2 index 7992c7c..b953ed6 100644 --- a/roles/homarr/templates/docker-compose.yml.j2 +++ b/roles/homarr/templates/docker-compose.yml.j2 @@ -2,17 +2,29 @@ # Homarr - A simple, yet powerful dashboard for your server. # #---------------------------------------------------------------------# services: - homarr: + {{ homarr_service_name }}: container_name: {{ homarr_service_name }} image: {{ homarr_image }} restart: unless-stopped volumes: {% if homarr_use_docker %} - - /var/run/docker.sock:/var/run/docker.sock # Optional, only if you want docker integration + - /var/run/docker.sock:/var/run/docker.sock {% endif %} - {{ homarr_docker_volume_dir }}/homarr/appdata:/appdata environment: - - SECRET_ENCRYPTION_KEY={{ homarr_secret_encryption_key }} + TZ: "Europe/Zurich" + BASE_URL: "{{ homarr_base_url }}" + NEXTAUTH_URL: "{{ homarr_base_url }}" + SECRET_ENCRYPTION_KEY: "{{ homarr_secret_encryption_key }}" + # Auth: Credentials + OIDC + AUTH_PROVIDERS: "credentials,oidc" + AUTH_OIDC_ISSUER: "{{ oidc_issuer }}" + AUTH_OIDC_CLIENT_ID: "{{ oidc_client_id }}" + AUTH_OIDC_CLIENT_SECRET: "{{ oidc_client_secret }}" + AUTH_OIDC_CLIENT_NAME: "{{ oidc_client_name | default('Keycloak') }}" + AUTH_OIDC_SCOPE_OVERWRITE: "{{ oidc_scopes | default('openid email profile groups') }}" + AUTH_OIDC_GROUPS_ATTRIBUTE: "{{ oidc_groups_attribute | default('groups') }}" + AUTH_OIDC_AUTO_LOGIN: "{{ oidc_auto_login | default('false') }}" networks: - {{ homarr_traefik_network }} labels: From c060d6136ab4c2b87ace631cf60c0b7007fbf4b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Tue, 12 May 2026 23:15:06 +0200 Subject: [PATCH 31/39] fix(homarr): salt column, bcrypt newline, transaction safety --- roles/homarr/tasks/main.yml | 219 +++++++++++++----------------------- 1 file changed, 78 insertions(+), 141 deletions(-) diff --git a/roles/homarr/tasks/main.yml b/roles/homarr/tasks/main.yml index f8dd3df..22bfe97 100644 --- a/roles/homarr/tasks/main.yml +++ b/roles/homarr/tasks/main.yml @@ -3,7 +3,22 @@ # tasks file for homarr # ===================================================================== -# 1. VORBEREITUNG: Pakete und Verzeichnisse VOR Container-Start +# 0. VALIDATION +# ===================================================================== + +- name: Validate encryption key + ansible.builtin.assert: + that: + - homarr_secret_encryption_key | length == 64 + - homarr_secret_encryption_key is match('^[a-f0-9]+$') + fail_msg: >- + homarr_secret_encryption_key must be a 64-character hex string. + Generate with: openssl rand -hex 32 + Provide via OpenBao, Ansible Vault or extra-vars. + success_msg: Encryption key validation passed + +# ===================================================================== +# 1. PREPARATION: packages and directories before container start # ===================================================================== - name: Ensure required packages are installed @@ -11,7 +26,6 @@ name: - sqlite3 - python3-docker - - python3-bcrypt state: present - name: Create docker compose directory @@ -37,7 +51,7 @@ register: db_exists # ===================================================================== -# 2. CONTAINER STARTEN +# 2. START CONTAINER # ===================================================================== - name: Create docker-compose file for homarr @@ -52,7 +66,7 @@ state: present # ===================================================================== -# 3. AUF DATENBANK WARTEN +# 3. WAIT FOR DATABASE # ===================================================================== - name: Wait for database to be created by Homarr @@ -63,71 +77,69 @@ when: not db_exists.stat.exists - name: Wait for database schema to be initialized - ansible.builtin.shell: | - i=0 - while [ $i -lt 30 ]; do - if sqlite3 "{{ homarr_db_dir }}" "SELECT name FROM sqlite_master WHERE type='table' AND name='board';" 2>/dev/null | grep -q board; then - exit 0 - fi - sleep 2 - i=$((i + 1)) - done - exit 1 + ansible.builtin.command: + cmd: sqlite3 "{{ homarr_db_dir }}" "SELECT name FROM sqlite_master WHERE type='table' AND name='board';" register: schema_check + until: schema_check.stdout == "board" + retries: 30 + delay: 2 changed_when: false when: not db_exists.stat.exists -- name: Ensure python3-bcrypt is installed - ansible.builtin.package: - name: python3-bcrypt - state: present +# ===================================================================== +# 4. GENERATE BCRYPT HASH (on controller, not on target) +# ===================================================================== - name: Generate bcrypt hash for admin password - ansible.builtin.shell: | - python3 -c " - import bcrypt - password = '{{ homarr_admin_password }}'.encode() - salt = bcrypt.gensalt(rounds=10) - hashed = bcrypt.hashpw(password, salt) - print(salt.decode()) - print(hashed.decode()) - " - register: bcrypt_output + ansible.builtin.shell: + cmd: python3 -c "import bcrypt, sys; print(bcrypt.hashpw(sys.stdin.read().encode(), bcrypt.gensalt(rounds=10)).decode())" + stdin: "{{ homarr_admin_password }}" + stdin_add_newline: false + delegate_to: localhost + become: false + register: bcrypt_result changed_when: false no_log: true +- name: Set bcrypt hash fact + ansible.builtin.set_fact: + homarr_bcrypt_hash: "{{ bcrypt_result.stdout }}" + no_log: true + # ===================================================================== -# 4. DATENBANK SEEDEN (nur wenn Onboarding noch nicht abgeschlossen) +# 5. SEED DATABASE (only if local admin user does not exist yet) # ===================================================================== -- name: Check if onboarding is already completed - ansible.builtin.shell: | - sqlite3 "{{ homarr_db_dir }}" "SELECT step FROM onboarding WHERE step='finish';" 2>/dev/null - register: onboarding_status +- name: Check if local admin user exists + ansible.builtin.command: + cmd: sqlite3 "{{ homarr_db_dir }}" "SELECT id FROM user WHERE id='user-local-admin';" + register: admin_exists changed_when: false failed_when: false - name: Seed Homarr database ansible.builtin.shell: | sqlite3 "{{ homarr_db_dir }}" << 'SEEDSQL' + BEGIN TRANSACTION; + -- SERVER SETTINGS INSERT OR REPLACE INTO serverSetting (setting_key, value) - VALUES - ('analytics', '{"json": {"enableGeneral": false, "enableWidgetData": false, "enableIntegrationData": false}}'), + VALUES + ('analytics', '{"json": {"enableGeneral": false, "enableWidgetData": false, "enableIntegrationData": false, "enableUserData": false}}'), ('culture', '{"json": {"defaultLocale": "de"}}'), ('crawling', '{"json": {"crawlingEnabled": false}}'), - ('board', '{"json": {"defaultBoardId": "board-default"}}'); + ('board', '{"json": {"homeBoardId": "board-default", "mobileHomeBoardId": "board-default", "enableStatusByDefault": true, "forceDisableStatus": false, "defaultBoardId": "board-default"}}'); - -- ONBOARDING ÜBERSPRINGEN + -- SKIP ONBOARDING UPDATE onboarding SET step = 'finish', previous_step = 'settings'; - -- ===================================================================== - -- GRUPPEN (müssen VOR groupMember existieren) - -- ===================================================================== + -- ================================================================= + -- GROUPS (must exist before groupMember) + -- ================================================================= - -- OIDC-ADMIN GRUPPE + -- OIDC admin group INSERT OR IGNORE INTO "group" (id, name, owner_id, position) - VALUES ('group-oidc-admins', '{{ oidc_admin_group | default("homarr-admins") }}', NULL, 0); + VALUES ('group-oidc-admins', '{{ homarr_oidc_admin_group }}', NULL, 0); INSERT OR IGNORE INTO groupPermission (group_id, permission) VALUES @@ -137,7 +149,7 @@ ('group-oidc-admins', 'integration-create'), ('group-oidc-admins', 'integration-full-access'); - -- CREDENTIALS-ADMIN GRUPPE + -- Credentials admin group INSERT OR IGNORE INTO "group" (id, name, owner_id, position) VALUES ('group-credentials-admin', 'credentials-admin', NULL, 1); @@ -149,31 +161,29 @@ ('group-credentials-admin', 'integration-create'), ('group-credentials-admin', 'integration-full-access'); - -- ===================================================================== - -- LOKALER ADMIN USER (Passwort wird via CLI gesetzt) - -- ===================================================================== + -- ================================================================= + -- LOCAL ADMIN USER + -- ================================================================= - INSERT OR IGNORE INTO user (id, name, email, password, salt, email_verified, provider) + INSERT OR IGNORE INTO user (id, name, email, password, email_verified, provider) VALUES ( 'user-local-admin', - '{{ homarr_admin_username | default("admin") }}', - '{{ homarr_admin_email | default("admin@digitalboard.ch") }}', - '{{ bcrypt_output.stdout_lines[1] }}', - '{{ bcrypt_output.stdout_lines[0] }}', + '{{ homarr_admin_username }}', + '{{ homarr_admin_email }}', + '{{ homarr_bcrypt_hash }}', 1, 'credentials' ); - -- ADMIN-USER DEN GRUPPEN ZUWEISEN (Gruppen existieren jetzt) - + -- Assign admin user to groups INSERT OR IGNORE INTO groupMember (group_id, user_id) VALUES ('group-credentials-admin', 'user-local-admin'), ('group-oidc-admins', 'user-local-admin'); - -- ===================================================================== + -- ================================================================= -- BOARD - -- ===================================================================== + -- ================================================================= INSERT OR IGNORE INTO board ( id, name, is_public, @@ -183,8 +193,8 @@ ) VALUES ( 'board-default', - '{{ default_board_name | default("Dashboard") }}', - {% if default_board_public | default(true) %}1{% else %}0{% endif %}, + '{{ homarr_default_board_name }}', + {% if homarr_default_board_public %}1{% else %}0{% endif %}, '#fa5252', '#fd7e14', 100, @@ -195,18 +205,18 @@ 0 ); - -- LAYOUTS + -- Layouts INSERT OR IGNORE INTO layout (id, name, board_id, column_count, breakpoint) VALUES ('layout-desktop', 'Desktop', 'board-default', 10, 0), ('layout-tablet', 'Tablet', 'board-default', 6, 768), ('layout-mobile', 'Mobile', 'board-default', 2, 480); - -- HOME BOARD FÜR ADMIN SETZEN (Board existiert jetzt) + -- Set home board for admin user (board exists now) UPDATE user SET home_board_id = 'board-default', mobile_home_board_id = 'board-default' WHERE id = 'user-local-admin'; - -- SEKTION + -- Section DELETE FROM section_layout WHERE section_id = 'section-apps'; DELETE FROM item_layout WHERE section_id = 'section-apps'; DELETE FROM section WHERE id = 'section-apps'; @@ -218,7 +228,7 @@ 'empty', 0, 0, - 'Anwendungen', + 'Applications', '{"json": {}}' ); @@ -228,15 +238,15 @@ ('section-apps', 'layout-tablet', NULL, 0, 0, 6, 4), ('section-apps', 'layout-mobile', NULL, 0, 0, 2, 6); - -- BOARD-BERECHTIGUNG + -- Board permissions INSERT OR IGNORE INTO boardGroupPermission (board_id, group_id, permission) VALUES ('board-default', 'group-oidc-admins', 'full-access'), ('board-default', 'group-credentials-admin', 'full-access'); - -- ===================================================================== + -- ================================================================= -- APPS - -- ===================================================================== + -- ================================================================= -- Nextcloud INSERT OR IGNORE INTO app (id, name, description, icon_url, href) @@ -312,85 +322,12 @@ ('item-mailman', 'section-apps', 'layout-desktop', 4, 0, 2, 1), ('item-mailman', 'section-apps', 'layout-tablet', 4, 0, 2, 1), ('item-mailman', 'section-apps', 'layout-mobile', 0, 1, 1, 1); + + COMMIT; SEEDSQL args: executable: /bin/bash register: seed_result changed_when: seed_result.rc == 0 - when: onboarding_status.stdout is not defined or 'finish' not in onboarding_status.stdout - -# ===================================================================== -# 5. RESTART, HEALTH-CHECK, DANN CLI -# ===================================================================== - -- name: Restart Homarr to apply database changes - ansible.builtin.shell: - cmd: docker compose restart - chdir: "{{ homarr_docker_compose_dir }}" - when: seed_result is changed - -- name: Wait for Homarr to be ready - ansible.builtin.shell: - cmd: docker compose exec -T {{ homarr_service_name }} wget -qO /dev/null "http://localhost:7575" 2>&1 - chdir: "{{ homarr_docker_compose_dir }}" - retries: 30 - delay: 5 - register: homarr_ready - until: homarr_ready.rc == 0 - changed_when: false - -- name: Display completion message - ansible.builtin.debug: - msg: | - ============================================================ - Homarr deployed! - URL: {{ homarr_base_url }} - Admin-User: {{ homarr_admin_username }} - Admin-Passwort: {{ homarr_admin_password }} - (Bitte sofort ändern!) - ============================================================ - when: not db_exists.stat.exists - -# ===================================================================== -# 6. ABSCHLUSS -# ===================================================================== - -- name: Display admin credentials - ansible.builtin.debug: - msg: | - ============================================ - LOKALER ADMIN PASSWORT (bitte sofort ändern!) - {{ admin_password_output.stdout }} - ============================================ - when: - - admin_password_output is defined - - admin_password_output.stdout is defined - - admin_password_output.stdout | length > 0 - -- name: Display completion message - ansible.builtin.debug: - msg: | - ============================================================ - Homarr wurde erfolgreich deployed! - ============================================================ - - URL: {{ homarr_base_url }} - - OIDC Konfiguration: - - Issuer: {{ oidc_issuer }} - - Client ID: {{ oidc_client_id }} - - Admin-Gruppe: {{ oidc_admin_group }} - - Nächste Schritte: - 1. Stelle sicher, dass in Keycloak die Gruppe "{{ oidc_admin_group }}" existiert - 2. Füge deinen User zur Gruppe "{{ oidc_admin_group }}" hinzu - 3. Öffne {{ homarr_base_url }} - 4. Du solltest automatisch via OIDC eingeloggt werden - - Das Standard-Board "{{ default_board_name }}" wurde erstellt mit: - - Nextcloud App - - Keycloak App - - Mailman App - - Weitere Apps kannst du direkt in der Homarr UI hinzufügen. - ============================================================ \ No newline at end of file + when: admin_exists.stdout == "" + notify: restart homarr \ No newline at end of file From bdb1b03a1890a65fe7b5c14c8a4c113db8f036cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Tue, 12 May 2026 23:15:53 +0200 Subject: [PATCH 32/39] refactor(homarr): align vars with homarr_ prefix, EN-only strings --- roles/homarr/defaults/main.yml | 41 ++++++++++++-------- roles/homarr/templates/docker-compose.yml.j2 | 19 +++++---- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index 78b32ab..dce7501 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -17,34 +17,43 @@ homarr_db_dir: "{{ homarr_appdata_dir }}/db/db.sqlite" # Service configuration homarr_domain: "homarr.local.test" homarr_image: "ghcr.io/homarr-labs/homarr:latest" -homarr_secret_encryption_key: "4fc2f54f54be3f4439b728da81b743fb0ee6317fd1a24f4096611f68019fa5a7" homarr_port: 7575 homarr_use_docker: false -# URL – wird für BASE_URL, NEXTAUTH_URL und die Completion-Message verwendet +# REQUIRED: 64-character hex string used to encrypt integration credentials. +# Generate with: openssl rand -hex 32 +# Provide via OpenBao lookup, Ansible Vault, or extra-vars. +# Never commit a real key to version control. +#homarr_secret_encryption_key: "" +homarr_secret_encryption_key: "4fc2f54f54be3f4439b728da81b743fb0ee6317fd1a24f4096611f68019fa5a7" + +# URL — used for BASE_URL, NEXTAUTH_URL and the completion message homarr_base_url: "https://home.local.test" -# OIDC Konfiguration -oidc_issuer: "https://auth.digitalboard.ch/realms/Digitalboard" -oidc_client_id: "homarr-digitalboard" -oidc_client_name: "Digitalboard" -oidc_scopes: "openid profile email groups" -oidc_groups_attribute: "groups" -oidc_client_secret: "mein-test-secret-aus-keycloak" -oidc_auto_login: "false" +# Auth providers (comma-separated): credentials, oidc, ldap +homarr_auth_providers: "credentials,oidc" -# OIDC Admin-Gruppe (muss in Keycloak existieren) -oidc_admin_group: "homarr-admins" +# OIDC configuration +homarr_oidc_issuer: "https://auth.digitalboard.ch/realms/Digitalboard" +homarr_oidc_client_id: "homarr-digitalboard" +homarr_oidc_client_name: "Digitalboard" +homarr_oidc_scopes: "openid profile email groups" +homarr_oidc_groups_attribute: "groups" +homarr_oidc_client_secret: "" +homarr_oidc_auto_login: "false" -# Board Konfiguration -default_board_name: "Home" -default_board_public: true +# OIDC admin group (must exist in the identity provider) +homarr_oidc_admin_group: "homarr-admins" + +# Board configuration +homarr_default_board_name: "Home" +homarr_default_board_public: true # Traefik configuration homarr_traefik_network: "proxy" homarr_use_ssl: true -# Lokaler Admin +# Local admin homarr_admin_username: "admin" homarr_admin_email: "admin@digitalboard.ch" homarr_admin_password: "ChangeMe123!" \ No newline at end of file diff --git a/roles/homarr/templates/docker-compose.yml.j2 b/roles/homarr/templates/docker-compose.yml.j2 index b953ed6..96c203d 100644 --- a/roles/homarr/templates/docker-compose.yml.j2 +++ b/roles/homarr/templates/docker-compose.yml.j2 @@ -1,5 +1,5 @@ #---------------------------------------------------------------------# -# Homarr - A simple, yet powerful dashboard for your server. # +# Homarr — A simple, yet powerful dashboard for your server. # #---------------------------------------------------------------------# services: {{ homarr_service_name }}: @@ -16,15 +16,14 @@ services: BASE_URL: "{{ homarr_base_url }}" NEXTAUTH_URL: "{{ homarr_base_url }}" SECRET_ENCRYPTION_KEY: "{{ homarr_secret_encryption_key }}" - # Auth: Credentials + OIDC - AUTH_PROVIDERS: "credentials,oidc" - AUTH_OIDC_ISSUER: "{{ oidc_issuer }}" - AUTH_OIDC_CLIENT_ID: "{{ oidc_client_id }}" - AUTH_OIDC_CLIENT_SECRET: "{{ oidc_client_secret }}" - AUTH_OIDC_CLIENT_NAME: "{{ oidc_client_name | default('Keycloak') }}" - AUTH_OIDC_SCOPE_OVERWRITE: "{{ oidc_scopes | default('openid email profile groups') }}" - AUTH_OIDC_GROUPS_ATTRIBUTE: "{{ oidc_groups_attribute | default('groups') }}" - AUTH_OIDC_AUTO_LOGIN: "{{ oidc_auto_login | default('false') }}" + AUTH_PROVIDERS: "{{ homarr_auth_providers }}" + AUTH_OIDC_ISSUER: "{{ homarr_oidc_issuer }}" + AUTH_OIDC_CLIENT_ID: "{{ homarr_oidc_client_id }}" + AUTH_OIDC_CLIENT_SECRET: "{{ homarr_oidc_client_secret }}" + AUTH_OIDC_CLIENT_NAME: "{{ homarr_oidc_client_name | default('Keycloak') }}" + AUTH_OIDC_SCOPE_OVERWRITE: "{{ homarr_oidc_scopes | default('openid email profile groups') }}" + AUTH_OIDC_GROUPS_ATTRIBUTE: "{{ homarr_oidc_groups_attribute | default('groups') }}" + AUTH_OIDC_AUTO_LOGIN: "{{ homarr_oidc_auto_login | default('false') }}" networks: - {{ homarr_traefik_network }} labels: From 123769a4f4d03f7f2bf18861f1a1bb8cfa868b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Tue, 12 May 2026 23:16:28 +0200 Subject: [PATCH 33/39] feat(homarr): use handler for restart, validate encryption key --- roles/homarr/handlers/main.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/roles/homarr/handlers/main.yml b/roles/homarr/handlers/main.yml index 56f5283..dedb949 100644 --- a/roles/homarr/handlers/main.yml +++ b/roles/homarr/handlers/main.yml @@ -1,3 +1,8 @@ #SPDX-License-Identifier: MIT-0 --- -# handlers file for homarr \ No newline at end of file +# handlers file for homarr + +- name: restart homarr + community.docker.docker_compose_v2: + project_src: "{{ homarr_docker_compose_dir }}" + state: restarted \ No newline at end of file From f4084ba078fd8fb82c079e9008c6fbf2c9db8a4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Tue, 12 May 2026 23:34:33 +0200 Subject: [PATCH 34/39] refactor(homarr): drop service_name var and rename db_dir to db - homarr_service_name removed, replaced with fixed "homarr" string - homarr_db_dir renamed to homarr_db (variable points to a file, not a dir) --- roles/homarr/defaults/main.yml | 9 ++++----- roles/homarr/tasks/main.yml | 10 +++++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index dce7501..2afb9f9 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -8,11 +8,10 @@ docker_volume_base_dir: /srv/data # homarr-specific configuration homarr_base_path: /srv/data/homarr -homarr_service_name: homarr -homarr_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ homarr_service_name }}" -homarr_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ homarr_service_name }}" -homarr_appdata_dir: "{{ homarr_docker_volume_dir }}/{{ homarr_service_name }}/appdata" -homarr_db_dir: "{{ homarr_appdata_dir }}/db/db.sqlite" +homarr_docker_compose_dir: "{{ docker_compose_base_dir }}/homarr" +homarr_docker_volume_dir: "{{ docker_volume_base_dir }}/homarr" +homarr_appdata_dir: "{{ homarr_docker_volume_dir }}/homarr/appdata" +homarr_db: "{{ homarr_appdata_dir }}/db/db.sqlite" # Service configuration homarr_domain: "homarr.local.test" diff --git a/roles/homarr/tasks/main.yml b/roles/homarr/tasks/main.yml index 22bfe97..b292408 100644 --- a/roles/homarr/tasks/main.yml +++ b/roles/homarr/tasks/main.yml @@ -47,7 +47,7 @@ - name: Check if database already exists ansible.builtin.stat: - path: "{{ homarr_db_dir }}" + path: "{{ homarr_db }}" register: db_exists # ===================================================================== @@ -71,14 +71,14 @@ - name: Wait for database to be created by Homarr ansible.builtin.wait_for: - path: "{{ homarr_db_dir }}" + path: "{{ homarr_db }}" state: present timeout: 60 when: not db_exists.stat.exists - name: Wait for database schema to be initialized ansible.builtin.command: - cmd: sqlite3 "{{ homarr_db_dir }}" "SELECT name FROM sqlite_master WHERE type='table' AND name='board';" + cmd: sqlite3 "{{ homarr_db }}" "SELECT name FROM sqlite_master WHERE type='table' AND name='board';" register: schema_check until: schema_check.stdout == "board" retries: 30 @@ -112,14 +112,14 @@ - name: Check if local admin user exists ansible.builtin.command: - cmd: sqlite3 "{{ homarr_db_dir }}" "SELECT id FROM user WHERE id='user-local-admin';" + cmd: sqlite3 "{{ homarr_db }}" "SELECT id FROM user WHERE id='user-local-admin';" register: admin_exists changed_when: false failed_when: false - name: Seed Homarr database ansible.builtin.shell: | - sqlite3 "{{ homarr_db_dir }}" << 'SEEDSQL' + sqlite3 "{{ homarr_db }}" << 'SEEDSQL' BEGIN TRANSACTION; -- SERVER SETTINGS From 3c35b8782ee75216d034ac63fa5f89fb912512c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Wed, 13 May 2026 13:58:15 +0200 Subject: [PATCH 35/39] fix: reomved remnants of removed env / fixed encription key validatiion --- roles/homarr/defaults/main.yml | 3 +-- roles/homarr/tasks/main.yml | 2 +- roles/homarr/templates/docker-compose.yml.j2 | 14 +++++++------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index 2afb9f9..a371bdf 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -23,8 +23,7 @@ homarr_use_docker: false # Generate with: openssl rand -hex 32 # Provide via OpenBao lookup, Ansible Vault, or extra-vars. # Never commit a real key to version control. -#homarr_secret_encryption_key: "" -homarr_secret_encryption_key: "4fc2f54f54be3f4439b728da81b743fb0ee6317fd1a24f4096611f68019fa5a7" +homarr_secret_encryption_key: "9981080f7eb054b90c7be80622608c3b63ba58408171ef3fbcdce9ba9c0554dc" # URL — used for BASE_URL, NEXTAUTH_URL and the completion message homarr_base_url: "https://home.local.test" diff --git a/roles/homarr/tasks/main.yml b/roles/homarr/tasks/main.yml index b292408..d3e9d75 100644 --- a/roles/homarr/tasks/main.yml +++ b/roles/homarr/tasks/main.yml @@ -10,7 +10,7 @@ ansible.builtin.assert: that: - homarr_secret_encryption_key | length == 64 - - homarr_secret_encryption_key is match('^[a-f0-9]+$') + - homarr_secret_encryption_key is match('^[a-fA-F0-9]+$') fail_msg: >- homarr_secret_encryption_key must be a 64-character hex string. Generate with: openssl rand -hex 32 diff --git a/roles/homarr/templates/docker-compose.yml.j2 b/roles/homarr/templates/docker-compose.yml.j2 index 96c203d..2d81063 100644 --- a/roles/homarr/templates/docker-compose.yml.j2 +++ b/roles/homarr/templates/docker-compose.yml.j2 @@ -2,8 +2,8 @@ # Homarr — A simple, yet powerful dashboard for your server. # #---------------------------------------------------------------------# services: - {{ homarr_service_name }}: - container_name: {{ homarr_service_name }} + homarr: + container_name: homarr image: {{ homarr_image }} restart: unless-stopped volumes: @@ -29,14 +29,14 @@ services: labels: - traefik.enable=true - traefik.docker.network={{ homarr_traefik_network }} - - traefik.http.routers.{{ homarr_service_name }}.rule=Host(`{{ homarr_domain }}`) + - traefik.http.routers.homarr.rule=Host(`{{ homarr_domain }}`) {% if homarr_use_ssl %} - - traefik.http.routers.{{ homarr_service_name }}.entrypoints=websecure - - traefik.http.routers.{{ homarr_service_name }}.tls=true + - traefik.http.routers.homarr.entrypoints=websecure + - traefik.http.routers.homarr.tls=true {% else %} - - traefik.http.routers.{{ homarr_service_name }}.entrypoints=web + - traefik.http.routers.homarr.entrypoints=web {% endif %} - - traefik.http.services.{{ homarr_service_name }}.loadbalancer.server.port={{ homarr_port }} + - traefik.http.services.homarr.loadbalancer.server.port={{ homarr_port }} networks: {{ homarr_traefik_network }}: external: true \ No newline at end of file From d4eaa5f12c814cc674b1cb7beca307f71c252cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Wed, 13 May 2026 14:51:36 +0200 Subject: [PATCH 36/39] refactor(homarr): extract seed SQL into template --- roles/homarr/defaults/main.yml | 2 +- roles/homarr/tasks/main.yml | 221 ++-------------------- roles/homarr/templates/homarr_seed.sql.j2 | 210 ++++++++++++++++++++ 3 files changed, 223 insertions(+), 210 deletions(-) create mode 100644 roles/homarr/templates/homarr_seed.sql.j2 diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index a371bdf..e981b1f 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -37,7 +37,7 @@ homarr_oidc_client_id: "homarr-digitalboard" homarr_oidc_client_name: "Digitalboard" homarr_oidc_scopes: "openid profile email groups" homarr_oidc_groups_attribute: "groups" -homarr_oidc_client_secret: "" +homarr_oidc_client_secret: "a91c9ec370f75fe34f7df20f50a70ff2d761ebd74c336a3f9e4640b49521cee2" homarr_oidc_auto_login: "false" # OIDC admin group (must exist in the identity provider) diff --git a/roles/homarr/tasks/main.yml b/roles/homarr/tasks/main.yml index d3e9d75..a3ff991 100644 --- a/roles/homarr/tasks/main.yml +++ b/roles/homarr/tasks/main.yml @@ -17,6 +17,15 @@ Provide via OpenBao, Ansible Vault or extra-vars. success_msg: Encryption key validation passed +- name: Validate OIDC configuration when enabled + ansible.builtin.assert: + that: + - homarr_oidc_client_secret | length > 0 + fail_msg: >- + homarr_oidc_client_secret must be set when 'oidc' is in homarr_auth_providers. + Set via OpenBao or remove 'oidc' from homarr_auth_providers. + when: "'oidc' in homarr_auth_providers" + # ===================================================================== # 1. PREPARATION: packages and directories before container start # ===================================================================== @@ -118,215 +127,9 @@ failed_when: false - name: Seed Homarr database - ansible.builtin.shell: | - sqlite3 "{{ homarr_db }}" << 'SEEDSQL' - BEGIN TRANSACTION; - - -- SERVER SETTINGS - INSERT OR REPLACE INTO serverSetting (setting_key, value) - VALUES - ('analytics', '{"json": {"enableGeneral": false, "enableWidgetData": false, "enableIntegrationData": false, "enableUserData": false}}'), - ('culture', '{"json": {"defaultLocale": "de"}}'), - ('crawling', '{"json": {"crawlingEnabled": false}}'), - ('board', '{"json": {"homeBoardId": "board-default", "mobileHomeBoardId": "board-default", "enableStatusByDefault": true, "forceDisableStatus": false, "defaultBoardId": "board-default"}}'); - - -- SKIP ONBOARDING - UPDATE onboarding SET step = 'finish', previous_step = 'settings'; - - -- ================================================================= - -- GROUPS (must exist before groupMember) - -- ================================================================= - - -- OIDC admin group - INSERT OR IGNORE INTO "group" (id, name, owner_id, position) - VALUES ('group-oidc-admins', '{{ homarr_oidc_admin_group }}', NULL, 0); - - INSERT OR IGNORE INTO groupPermission (group_id, permission) - VALUES - ('group-oidc-admins', 'admin'), - ('group-oidc-admins', 'board-create'), - ('group-oidc-admins', 'board-full-access'), - ('group-oidc-admins', 'integration-create'), - ('group-oidc-admins', 'integration-full-access'); - - -- Credentials admin group - INSERT OR IGNORE INTO "group" (id, name, owner_id, position) - VALUES ('group-credentials-admin', 'credentials-admin', NULL, 1); - - INSERT OR IGNORE INTO groupPermission (group_id, permission) - VALUES - ('group-credentials-admin', 'admin'), - ('group-credentials-admin', 'board-create'), - ('group-credentials-admin', 'board-full-access'), - ('group-credentials-admin', 'integration-create'), - ('group-credentials-admin', 'integration-full-access'); - - -- ================================================================= - -- LOCAL ADMIN USER - -- ================================================================= - - INSERT OR IGNORE INTO user (id, name, email, password, email_verified, provider) - VALUES ( - 'user-local-admin', - '{{ homarr_admin_username }}', - '{{ homarr_admin_email }}', - '{{ homarr_bcrypt_hash }}', - 1, - 'credentials' - ); - - -- Assign admin user to groups - INSERT OR IGNORE INTO groupMember (group_id, user_id) - VALUES - ('group-credentials-admin', 'user-local-admin'), - ('group-oidc-admins', 'user-local-admin'); - - -- ================================================================= - -- BOARD - -- ================================================================= - - INSERT OR IGNORE INTO board ( - id, name, is_public, - primary_color, secondary_color, opacity, - background_image_attachment, background_image_repeat, background_image_size, - item_radius, disable_status - ) - VALUES ( - 'board-default', - '{{ homarr_default_board_name }}', - {% if homarr_default_board_public %}1{% else %}0{% endif %}, - '#fa5252', - '#fd7e14', - 100, - 'fixed', - 'no-repeat', - 'cover', - 'lg', - 0 - ); - - -- Layouts - INSERT OR IGNORE INTO layout (id, name, board_id, column_count, breakpoint) - VALUES - ('layout-desktop', 'Desktop', 'board-default', 10, 0), - ('layout-tablet', 'Tablet', 'board-default', 6, 768), - ('layout-mobile', 'Mobile', 'board-default', 2, 480); - - -- Set home board for admin user (board exists now) - UPDATE user SET home_board_id = 'board-default', mobile_home_board_id = 'board-default' - WHERE id = 'user-local-admin'; - - -- Section - DELETE FROM section_layout WHERE section_id = 'section-apps'; - DELETE FROM item_layout WHERE section_id = 'section-apps'; - DELETE FROM section WHERE id = 'section-apps'; - - INSERT INTO section (id, board_id, kind, x_offset, y_offset, name, options) - VALUES ( - 'section-apps', - 'board-default', - 'empty', - 0, - 0, - 'Applications', - '{"json": {}}' - ); - - INSERT OR REPLACE INTO section_layout (section_id, layout_id, parent_section_id, x_offset, y_offset, width, height) - VALUES - ('section-apps', 'layout-desktop', NULL, 0, 0, 10, 3), - ('section-apps', 'layout-tablet', NULL, 0, 0, 6, 4), - ('section-apps', 'layout-mobile', NULL, 0, 0, 2, 6); - - -- Board permissions - INSERT OR IGNORE INTO boardGroupPermission (board_id, group_id, permission) - VALUES - ('board-default', 'group-oidc-admins', 'full-access'), - ('board-default', 'group-credentials-admin', 'full-access'); - - -- ================================================================= - -- APPS - -- ================================================================= - - -- Nextcloud - INSERT OR IGNORE INTO app (id, name, description, icon_url, href) - VALUES ( - 'app-nextcloud', - 'Nextcloud', - 'Cloud Storage & Collaboration', - 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png', - 'https://cloud.digitalboard.ch' - ); - - INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) - VALUES ( - 'item-nextcloud', - 'board-default', - 'app', - '{"json": {"appId": "app-nextcloud"}}', - '{"json": {}}' - ); - - INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) - VALUES - ('item-nextcloud', 'section-apps', 'layout-desktop', 0, 0, 2, 1), - ('item-nextcloud', 'section-apps', 'layout-tablet', 0, 0, 2, 1), - ('item-nextcloud', 'section-apps', 'layout-mobile', 0, 0, 1, 1); - - -- Keycloak - INSERT OR IGNORE INTO app (id, name, description, icon_url, href) - VALUES ( - 'app-keycloak', - 'Keycloak', - 'Identity & Access Management', - 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/keycloak.png', - 'https://auth.digitalboard.ch' - ); - - INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) - VALUES ( - 'item-keycloak', - 'board-default', - 'app', - '{"json": {"appId": "app-keycloak"}}', - '{"json": {}}' - ); - - INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) - VALUES - ('item-keycloak', 'section-apps', 'layout-desktop', 2, 0, 2, 1), - ('item-keycloak', 'section-apps', 'layout-tablet', 2, 0, 2, 1), - ('item-keycloak', 'section-apps', 'layout-mobile', 1, 0, 1, 1); - - -- Mailman - INSERT OR IGNORE INTO app (id, name, description, icon_url, href) - VALUES ( - 'app-mailman', - 'Mailman', - 'Mailing List Manager', - 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/mailman.png', - 'https://lists.digitalboard.ch' - ); - - INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) - VALUES ( - 'item-mailman', - 'board-default', - 'app', - '{"json": {"appId": "app-mailman"}}', - '{"json": {}}' - ); - - INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) - VALUES - ('item-mailman', 'section-apps', 'layout-desktop', 4, 0, 2, 1), - ('item-mailman', 'section-apps', 'layout-tablet', 4, 0, 2, 1), - ('item-mailman', 'section-apps', 'layout-mobile', 0, 1, 1, 1); - - COMMIT; - SEEDSQL - args: - executable: /bin/bash + ansible.builtin.command: + cmd: sqlite3 "{{ homarr_db }}" + stdin: "{{ lookup('template', 'homarr_seed.sql.j2') }}" register: seed_result changed_when: seed_result.rc == 0 when: admin_exists.stdout == "" diff --git a/roles/homarr/templates/homarr_seed.sql.j2 b/roles/homarr/templates/homarr_seed.sql.j2 new file mode 100644 index 0000000..0490c04 --- /dev/null +++ b/roles/homarr/templates/homarr_seed.sql.j2 @@ -0,0 +1,210 @@ +BEGIN TRANSACTION; + +-- ===================================================================== +-- SERVER SETTINGS +-- ===================================================================== + +INSERT OR REPLACE INTO serverSetting (setting_key, value) +VALUES + ('analytics', '{"json": {"enableGeneral": false, "enableWidgetData": false, "enableIntegrationData": false, "enableUserData": false}}'), + ('culture', '{"json": {"defaultLocale": "de"}}'), + ('crawling', '{"json": {"crawlingEnabled": false}}'), + ('board', '{"json": {"homeBoardId": "board-default", "mobileHomeBoardId": "board-default", "enableStatusByDefault": true, "forceDisableStatus": false, "defaultBoardId": "board-default"}}'); + +-- Skip onboarding wizard +UPDATE onboarding SET step = 'finish', previous_step = 'settings'; + +-- ===================================================================== +-- GROUPS (must exist before groupMember) +-- ===================================================================== + +-- OIDC admin group +INSERT OR IGNORE INTO "group" (id, name, owner_id, position) +VALUES ('group-oidc-admins', '{{ homarr_oidc_admin_group }}', NULL, 0); + +INSERT OR IGNORE INTO groupPermission (group_id, permission) +VALUES + ('group-oidc-admins', 'admin'), + ('group-oidc-admins', 'board-create'), + ('group-oidc-admins', 'board-full-access'), + ('group-oidc-admins', 'integration-create'), + ('group-oidc-admins', 'integration-full-access'); + +-- Credentials admin group +INSERT OR IGNORE INTO "group" (id, name, owner_id, position) +VALUES ('group-credentials-admin', 'credentials-admin', NULL, 1); + +INSERT OR IGNORE INTO groupPermission (group_id, permission) +VALUES + ('group-credentials-admin', 'admin'), + ('group-credentials-admin', 'board-create'), + ('group-credentials-admin', 'board-full-access'), + ('group-credentials-admin', 'integration-create'), + ('group-credentials-admin', 'integration-full-access'); + +-- ===================================================================== +-- LOCAL ADMIN USER +-- ===================================================================== + +INSERT OR IGNORE INTO user (id, name, email, password, email_verified, provider) +VALUES ( + 'user-local-admin', + '{{ homarr_admin_username }}', + '{{ homarr_admin_email }}', + '{{ homarr_bcrypt_hash }}', + 1, + 'credentials' +); + +-- Assign admin user to groups +INSERT OR IGNORE INTO groupMember (group_id, user_id) +VALUES + ('group-credentials-admin', 'user-local-admin'), + ('group-oidc-admins', 'user-local-admin'); + +-- ===================================================================== +-- BOARD +-- ===================================================================== + +INSERT OR IGNORE INTO board ( + id, name, is_public, + primary_color, secondary_color, opacity, + background_image_attachment, background_image_repeat, background_image_size, + item_radius, disable_status +) +VALUES ( + 'board-default', + '{{ homarr_default_board_name }}', + {% if homarr_default_board_public %}1{% else %}0{% endif %}, + '#fa5252', + '#fd7e14', + 100, + 'fixed', + 'no-repeat', + 'cover', + 'lg', + 0 +); + +-- Layouts +INSERT OR IGNORE INTO layout (id, name, board_id, column_count, breakpoint) +VALUES + ('layout-desktop', 'Desktop', 'board-default', 10, 0), + ('layout-tablet', 'Tablet', 'board-default', 6, 768), + ('layout-mobile', 'Mobile', 'board-default', 2, 480); + +-- Set home board for admin user (board exists now) +UPDATE user SET home_board_id = 'board-default', mobile_home_board_id = 'board-default' +WHERE id = 'user-local-admin'; + +-- ===================================================================== +-- SECTION +-- ===================================================================== + +DELETE FROM section_layout WHERE section_id = 'section-apps'; +DELETE FROM item_layout WHERE section_id = 'section-apps'; +DELETE FROM section WHERE id = 'section-apps'; + +INSERT INTO section (id, board_id, kind, x_offset, y_offset, name, options) +VALUES ( + 'section-apps', + 'board-default', + 'empty', + 0, + 0, + 'Applications', + '{"json": {}}' +); + +INSERT OR REPLACE INTO section_layout (section_id, layout_id, parent_section_id, x_offset, y_offset, width, height) +VALUES + ('section-apps', 'layout-desktop', NULL, 0, 0, 10, 3), + ('section-apps', 'layout-tablet', NULL, 0, 0, 6, 4), + ('section-apps', 'layout-mobile', NULL, 0, 0, 2, 6); + +-- Board permissions +INSERT OR IGNORE INTO boardGroupPermission (board_id, group_id, permission) +VALUES + ('board-default', 'group-oidc-admins', 'full-access'), + ('board-default', 'group-credentials-admin', 'full-access'); + +-- ===================================================================== +-- APPS +-- ===================================================================== + +-- Nextcloud +INSERT OR IGNORE INTO app (id, name, description, icon_url, href) +VALUES ( + 'app-nextcloud', + 'Nextcloud', + 'Cloud Storage & Collaboration', + 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png', + 'https://cloud.digitalboard.ch' +); + +INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) +VALUES ( + 'item-nextcloud', + 'board-default', + 'app', + '{"json": {"appId": "app-nextcloud"}}', + '{"json": {}}' +); + +INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) +VALUES + ('item-nextcloud', 'section-apps', 'layout-desktop', 0, 0, 2, 1), + ('item-nextcloud', 'section-apps', 'layout-tablet', 0, 0, 2, 1), + ('item-nextcloud', 'section-apps', 'layout-mobile', 0, 0, 1, 1); + +-- Keycloak +INSERT OR IGNORE INTO app (id, name, description, icon_url, href) +VALUES ( + 'app-keycloak', + 'Keycloak', + 'Identity & Access Management', + 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/keycloak.png', + 'https://auth.digitalboard.ch' +); + +INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) +VALUES ( + 'item-keycloak', + 'board-default', + 'app', + '{"json": {"appId": "app-keycloak"}}', + '{"json": {}}' +); + +INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) +VALUES + ('item-keycloak', 'section-apps', 'layout-desktop', 2, 0, 2, 1), + ('item-keycloak', 'section-apps', 'layout-tablet', 2, 0, 2, 1), + ('item-keycloak', 'section-apps', 'layout-mobile', 1, 0, 1, 1); + +-- Mailman +INSERT OR IGNORE INTO app (id, name, description, icon_url, href) +VALUES ( + 'app-mailman', + 'Mailman', + 'Mailing List Manager', + 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/mailman.png', + 'https://lists.digitalboard.ch' +); + +INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) +VALUES ( + 'item-mailman', + 'board-default', + 'app', + '{"json": {"appId": "app-mailman"}}', + '{"json": {}}' +); + +INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) +VALUES + ('item-mailman', 'section-apps', 'layout-desktop', 4, 0, 2, 1), + ('item-mailman', 'section-apps', 'layout-tablet', 4, 0, 2, 1), + ('item-mailman', 'section-apps', 'layout-mobile', 0, 1, 1, 1); + +COMMIT; From c1c1a8459100ea201f46e5fcf9cb02f16b81ab76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Wed, 13 May 2026 15:02:32 +0200 Subject: [PATCH 37/39] feat(homarr): make apps list configurable with auto-layout --- roles/homarr/defaults/main.yml | 30 +++++- roles/homarr/tasks/main.yml | 12 +++ roles/homarr/templates/homarr_seed.sql.j2 | 119 +++++++++++----------- 3 files changed, 101 insertions(+), 60 deletions(-) diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index e981b1f..2ef6df3 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -54,4 +54,32 @@ homarr_use_ssl: true # Local admin homarr_admin_username: "admin" homarr_admin_email: "admin@digitalboard.ch" -homarr_admin_password: "ChangeMe123!" \ No newline at end of file +homarr_admin_password: "ChangeMe123!" + +# Applications shown on the default board. +# Each app needs id, name, description, icon, href and a width (1-10). +# Height defaults to 1, can be increased for taller tiles. +# Apps are automatically packed left-to-right into the desktop grid (10 cols), +# scaled to tablet (6 cols) and mobile (2 cols). +homarr_apps: + - id: nextcloud + name: Nextcloud + description: Cloud Storage & Collaboration + icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png + href: https://cloud.digitalboard.ch + width: 2 + height: 1 + - id: keycloak + name: Keycloak + description: Identity & Access Management + icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/keycloak.png + href: https://auth.digitalboard.ch + width: 2 + height: 1 + - id: mailman + name: Mailman + description: Mailing List Manager + icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/mailman.png + href: https://lists.digitalboard.ch + width: 2 + height: 1 \ No newline at end of file diff --git a/roles/homarr/tasks/main.yml b/roles/homarr/tasks/main.yml index a3ff991..06488f4 100644 --- a/roles/homarr/tasks/main.yml +++ b/roles/homarr/tasks/main.yml @@ -26,6 +26,18 @@ Set via OpenBao or remove 'oidc' from homarr_auth_providers. when: "'oidc' in homarr_auth_providers" +- name: Validate homarr_apps have unique ids + ansible.builtin.assert: + that: + - homarr_apps | map(attribute='id') | list | length == + homarr_apps | map(attribute='id') | unique | list | length + fail_msg: >- + homarr_apps contains duplicate ids. + Each app must have a unique 'id'. Got: + {{ homarr_apps | map(attribute='id') | list }} + success_msg: All app ids are unique + when: homarr_apps | length > 0 + # ===================================================================== # 1. PREPARATION: packages and directories before container start # ===================================================================== diff --git a/roles/homarr/templates/homarr_seed.sql.j2 b/roles/homarr/templates/homarr_seed.sql.j2 index 0490c04..fdb1a2f 100644 --- a/roles/homarr/templates/homarr_seed.sql.j2 +++ b/roles/homarr/templates/homarr_seed.sql.j2 @@ -1,3 +1,35 @@ +{#- + Auto-layout packing macro. + + Greedy left-to-right packing of apps into a grid with `cols` columns. + Returns the list of apps with computed x/y/w/h fields. + + Width is clamped to cols (so an app wider than the grid is downsized + rather than overflowing). Height is taken as-is. +-#} +{%- macro pack(apps, cols) -%} + {%- set ns = namespace(x=0, y=0, row_h=0, out=[]) -%} + {%- for app in apps -%} + {%- set w = [app.width, cols] | min -%} + {%- set h = app.height | default(1) -%} + {%- if ns.x + w > cols -%} + {%- set ns.x = 0 -%} + {%- set ns.y = ns.y + ns.row_h -%} + {%- set ns.row_h = 0 -%} + {%- endif -%} + {%- set _ = ns.out.append({'id': app.id, 'x': ns.x, 'y': ns.y, 'w': w, 'h': h}) -%} + {%- set ns.x = ns.x + w -%} + {%- if h > ns.row_h -%} + {%- set ns.row_h = h -%} + {%- endif -%} + {%- endfor -%} + {{- ns.out | to_json -}} +{%- endmacro -%} + +{%- set desktop_layout = pack(homarr_apps, 10) | from_json -%} +{%- set tablet_layout = pack(homarr_apps, 6) | from_json -%} +{%- set mobile_layout = pack(homarr_apps, 2) | from_json -%} + BEGIN TRANSACTION; -- ===================================================================== @@ -129,82 +161,51 @@ VALUES ('board-default', 'group-credentials-admin', 'full-access'); -- ===================================================================== --- APPS +-- APPS (auto-generated from homarr_apps variable) -- ===================================================================== --- Nextcloud +{% if homarr_apps | length > 0 %} +{% for app in homarr_apps %} INSERT OR IGNORE INTO app (id, name, description, icon_url, href) VALUES ( - 'app-nextcloud', - 'Nextcloud', - 'Cloud Storage & Collaboration', - 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png', - 'https://cloud.digitalboard.ch' + 'app-{{ app.id }}', + '{{ app.name }}', + '{{ app.description | default("") }}', + '{{ app.icon }}', + '{{ app.href }}' ); INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) VALUES ( - 'item-nextcloud', + 'item-{{ app.id }}', 'board-default', 'app', - '{"json": {"appId": "app-nextcloud"}}', + '{"json": {"appId": "app-{{ app.id }}"}}', '{"json": {}}' ); -INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) -VALUES - ('item-nextcloud', 'section-apps', 'layout-desktop', 0, 0, 2, 1), - ('item-nextcloud', 'section-apps', 'layout-tablet', 0, 0, 2, 1), - ('item-nextcloud', 'section-apps', 'layout-mobile', 0, 0, 1, 1); - --- Keycloak -INSERT OR IGNORE INTO app (id, name, description, icon_url, href) -VALUES ( - 'app-keycloak', - 'Keycloak', - 'Identity & Access Management', - 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/keycloak.png', - 'https://auth.digitalboard.ch' -); - -INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) -VALUES ( - 'item-keycloak', - 'board-default', - 'app', - '{"json": {"appId": "app-keycloak"}}', - '{"json": {}}' -); +{% endfor %} INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) VALUES - ('item-keycloak', 'section-apps', 'layout-desktop', 2, 0, 2, 1), - ('item-keycloak', 'section-apps', 'layout-tablet', 2, 0, 2, 1), - ('item-keycloak', 'section-apps', 'layout-mobile', 1, 0, 1, 1); - --- Mailman -INSERT OR IGNORE INTO app (id, name, description, icon_url, href) -VALUES ( - 'app-mailman', - 'Mailman', - 'Mailing List Manager', - 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/mailman.png', - 'https://lists.digitalboard.ch' -); - -INSERT OR IGNORE INTO item (id, board_id, kind, options, advanced_options) -VALUES ( - 'item-mailman', - 'board-default', - 'app', - '{"json": {"appId": "app-mailman"}}', - '{"json": {}}' -); +{% for entry in desktop_layout %} + ('item-{{ entry.id }}', 'section-apps', 'layout-desktop', {{ entry.x }}, {{ entry.y }}, {{ entry.w }}, {{ entry.h }}){% if not loop.last %},{% endif %} +{% endfor %} +; INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) VALUES - ('item-mailman', 'section-apps', 'layout-desktop', 4, 0, 2, 1), - ('item-mailman', 'section-apps', 'layout-tablet', 4, 0, 2, 1), - ('item-mailman', 'section-apps', 'layout-mobile', 0, 1, 1, 1); +{% for entry in tablet_layout %} + ('item-{{ entry.id }}', 'section-apps', 'layout-tablet', {{ entry.x }}, {{ entry.y }}, {{ entry.w }}, {{ entry.h }}){% if not loop.last %},{% endif %} +{% endfor %} +; -COMMIT; +INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) +VALUES +{% for entry in mobile_layout %} + ('item-{{ entry.id }}', 'section-apps', 'layout-mobile', {{ entry.x }}, {{ entry.y }}, {{ entry.w }}, {{ entry.h }}){% if not loop.last %},{% endif %} +{% endfor %} +; +{% endif %} + +COMMIT; \ No newline at end of file From 308bf5012222e149e4e2fce9a27619ea24029433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Wed, 13 May 2026 15:12:55 +0200 Subject: [PATCH 38/39] chore(homarr): remove digitalboard-specific defaults --- roles/homarr/defaults/main.yml | 37 ++++++++++++---------------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index 2ef6df3..24e6b70 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -57,29 +57,18 @@ homarr_admin_email: "admin@digitalboard.ch" homarr_admin_password: "ChangeMe123!" # Applications shown on the default board. -# Each app needs id, name, description, icon, href and a width (1-10). -# Height defaults to 1, can be increased for taller tiles. +# Override in your project/inventory vars. Each app needs: +# id, name, icon, href, width (1-10). Optional: description, height (default 1). # Apps are automatically packed left-to-right into the desktop grid (10 cols), # scaled to tablet (6 cols) and mobile (2 cols). -homarr_apps: - - id: nextcloud - name: Nextcloud - description: Cloud Storage & Collaboration - icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png - href: https://cloud.digitalboard.ch - width: 2 - height: 1 - - id: keycloak - name: Keycloak - description: Identity & Access Management - icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/keycloak.png - href: https://auth.digitalboard.ch - width: 2 - height: 1 - - id: mailman - name: Mailman - description: Mailing List Manager - icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/mailman.png - href: https://lists.digitalboard.ch - width: 2 - height: 1 \ No newline at end of file +# +# Example: +# homarr_apps: +# - id: nextcloud +# name: Nextcloud +# description: Cloud Storage & Collaboration +# icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png +# href: https://cloud.example.com +# width: 2 +# height: 1 +homarr_apps: [ ] \ No newline at end of file From 2aa1df861405c0e4e5f4e6d8371e4970381874af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20W=C3=BCst?= Date: Wed, 13 May 2026 15:24:51 +0200 Subject: [PATCH 39/39] chore(homarr): added readme and removed test env contents --- roles/homarr/README.md | 206 +++++++++++++++++++++++++++++---- roles/homarr/defaults/main.yml | 21 ++-- 2 files changed, 193 insertions(+), 34 deletions(-) diff --git a/roles/homarr/README.md b/roles/homarr/README.md index da76bcd..db0ed4d 100644 --- a/roles/homarr/README.md +++ b/roles/homarr/README.md @@ -1,38 +1,196 @@ -Role Name -========= +# homarr -A brief description of the role goes here. +Deploy [Homarr](https://github.com/homarr-labs/homarr) as a self-contained +Docker Compose stack behind Traefik, with seeded admin user, OIDC group +and customizable application tiles. -Requirements ------------- +## What this role does -Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. +- Deploys the official Homarr container with Traefik labels +- Seeds the SQLite database with: + - server settings (locale, analytics, crawling, default board) + - a default board with the three layouts (desktop/tablet/mobile) + - a local admin user with bcrypt-hashed password + - OIDC and credentials admin groups with full permissions + - application tiles defined in `homarr_apps`, auto-laid-out across all + three screen sizes +- Skips the onboarding wizard so the instance is usable right after deploy +- Restarts the container via handler when the seed or compose file changes -Role Variables --------------- +## What this role does NOT do -A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. +- Does not configure OIDC end-to-end — set `homarr_oidc_*` variables and + configure the corresponding client in your identity provider +- Does not migrate existing Homarr databases — only seeds empty ones +- Does not create users beyond the single local admin (OIDC users are + provisioned on first login) -Dependencies ------------- +## Required variables -A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. +Provide via OpenBao, Ansible Vault, or extra-vars. **Never commit real +secrets to version control.** -Example Playbook ----------------- +| Variable | Format | Generate with | +|---|---|---| +| `homarr_secret_encryption_key` | 64-char hex string | `openssl rand -hex 32` | +| `homarr_admin_password` | strong password | `openssl rand -base64 24` | +| `homarr_oidc_client_secret` | from your identity provider | — | -Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: +The `assert` task at the top of the role will fail fast if the encryption +key is missing or malformed. - - hosts: servers - roles: - - { role: username.rolename, x: 42 } +## Configurable variables -License -------- +See `defaults/main.yml` for the full list. Most useful overrides: -BSD +| Variable | Default | Purpose | +|---|---|---| +| `homarr_domain` | `homarr.local.test` | Traefik Host rule | +| `homarr_base_url` | `https://home.local.test` | NEXTAUTH_URL / BASE_URL | +| `homarr_auth_providers` | `credentials` | `credentials`, `oidc`, or both | +| `homarr_oidc_issuer` | empty | Identity provider issuer URL | +| `homarr_oidc_client_id` | empty | OIDC client id | +| `homarr_oidc_admin_group` | `homarr-admins` | Group granting admin role | +| `homarr_apps` | `[]` | List of application tiles, see below | -Author Information ------------------- +## Application tiles -An optional section for the role authors to include contact information, or a website (HTML is not allowed). \ No newline at end of file +`homarr_apps` is a list of tile definitions that are seeded into the +default board. Each entry needs: + +| Field | Required | Description | +|---|---|---| +| `id` | yes | Unique slug, used as `app-` and `item-` | +| `name` | yes | Display name | +| `icon` | yes | Icon URL | +| `href` | yes | Click target | +| `width` | yes | Tile width in grid cells (1–10) | +| `description` | no | Tooltip / subtitle | +| `height` | no | Tile height (default `1`) | + +The role validates that all `id` values are unique. + +### Auto-layout + +Tiles are packed left-to-right into three layouts: + +- **Desktop**: 10 columns +- **Tablet**: 6 columns +- **Mobile**: 2 columns + +When a tile does not fit the remaining width of a row, it wraps to the +next row. Tile width is clamped to the grid width (a tile with +`width: 8` becomes `width: 6` on tablet and `width: 2` on mobile). + +### Example + +```yaml +homarr_apps: + - id: nextcloud + name: Nextcloud + description: Cloud Storage & Collaboration + icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png + href: https://cloud.example.com + width: 2 + - id: keycloak + name: Keycloak + description: Identity & Access Management + icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/keycloak.png + href: https://auth.example.com + width: 2 +``` + +## First login + +After the role completes, log in at `{{ homarr_base_url }}` with: + +- Username: value of `homarr_admin_username` (default `admin`) +- Password: value of `homarr_admin_password` + +OIDC users are provisioned on first login if their identity provider +group matches `homarr_oidc_admin_group`. They receive admin permissions +automatically through the seeded `group-oidc-admins` group. + +## Example playbook + +```yaml +- name: Deploy Homarr service + hosts: homarr_servers + become: true + roles: + - digitalboard.core.homarr +``` + +With inventory variables: + +```yaml +# inventories//group_vars/homarr_servers.yml +homarr_domain: home.digitalboard.ch +homarr_base_url: "https://home.digitalboard.ch" + +homarr_auth_providers: "credentials,oidc" +homarr_oidc_issuer: "https://auth.digitalboard.ch/realms/Digitalboard" +homarr_oidc_client_id: "homarr-digitalboard" +homarr_oidc_client_name: "Digitalboard" + +homarr_secret_encryption_key: >- + {{ lookup('community.hashi_vault.vault_kv2_get', + 'digitalboard/homarr', + mount_point='kv').data.data.encryption_key }} +homarr_admin_password: >- + {{ lookup('community.hashi_vault.vault_kv2_get', + 'digitalboard/homarr', + mount_point='kv').data.data.admin_password }} +homarr_oidc_client_secret: >- + {{ lookup('community.hashi_vault.vault_kv2_get', + 'digitalboard/homarr', + mount_point='kv').data.data.oidc_client_secret }} + +homarr_apps: + - id: nextcloud + name: Nextcloud + description: Cloud Storage + icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/nextcloud.png + href: https://cloud.digitalboard.ch + width: 2 + - id: keycloak + name: Keycloak + description: Identity & Access Management + icon: https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/keycloak.png + href: https://auth.digitalboard.ch + width: 2 +``` + +## Re-running the role + +The role is idempotent for the typical re-run case: + +- The seed only runs when no local admin user exists in the database +- The compose file and seed template are deployed via `template`, + which only changes content when the inputs change +- The restart handler only fires when one of those templates changes + +If you need to re-seed an existing database (for example after deleting +the database file to apply schema changes), the role will detect the +fresh database and seed it again on the next run. + +## Troubleshooting + +**Login fails after deploy.** Verify that the bcrypt hash was written +correctly: + +```bash +sqlite3 /srv/data/homarr/homarr/appdata/db/db.sqlite \ + "SELECT id, name, email, length(password), provider FROM user;" +``` + +Expected: one row with `user-local-admin`, password length 60, +provider `credentials`. + +**Encryption key validation fails.** The key must be exactly 64 +characters and contain only hex digits (`[a-fA-F0-9]`). Both upper- +and lowercase are accepted. + +**App tiles overlap.** Check `homarr_apps` for duplicate `id` values. +The role validates this, but if you bypass the check, the seed will +still run and Homarr will display only one of the duplicates. \ No newline at end of file diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index 24e6b70..728e3b3 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -1,3 +1,4 @@ +homarr_apps: [ ] #SPDX-License-Identifier: MIT-0 --- # defaults file for homarr @@ -23,21 +24,21 @@ homarr_use_docker: false # Generate with: openssl rand -hex 32 # Provide via OpenBao lookup, Ansible Vault, or extra-vars. # Never commit a real key to version control. -homarr_secret_encryption_key: "9981080f7eb054b90c7be80622608c3b63ba58408171ef3fbcdce9ba9c0554dc" +homarr_secret_encryption_key: "" # URL — used for BASE_URL, NEXTAUTH_URL and the completion message homarr_base_url: "https://home.local.test" # Auth providers (comma-separated): credentials, oidc, ldap -homarr_auth_providers: "credentials,oidc" +homarr_auth_providers: "credentials" -# OIDC configuration -homarr_oidc_issuer: "https://auth.digitalboard.ch/realms/Digitalboard" -homarr_oidc_client_id: "homarr-digitalboard" -homarr_oidc_client_name: "Digitalboard" +# OIDC configuration (only used when 'oidc' is in homarr_auth_providers) +homarr_oidc_issuer: "" +homarr_oidc_client_id: "" +homarr_oidc_client_name: "" homarr_oidc_scopes: "openid profile email groups" homarr_oidc_groups_attribute: "groups" -homarr_oidc_client_secret: "a91c9ec370f75fe34f7df20f50a70ff2d761ebd74c336a3f9e4640b49521cee2" +homarr_oidc_client_secret: "" homarr_oidc_auto_login: "false" # OIDC admin group (must exist in the identity provider) @@ -51,9 +52,9 @@ homarr_default_board_public: true homarr_traefik_network: "proxy" homarr_use_ssl: true -# Local admin +# Local admin (override in inventory or via vault) homarr_admin_username: "admin" -homarr_admin_email: "admin@digitalboard.ch" +homarr_admin_email: "admin@example.com" homarr_admin_password: "ChangeMe123!" # Applications shown on the default board. @@ -71,4 +72,4 @@ homarr_admin_password: "ChangeMe123!" # href: https://cloud.example.com # width: 2 # height: 1 -homarr_apps: [ ] \ No newline at end of file +homarr_apps: [] \ No newline at end of file