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..82890ad --- /dev/null +++ b/roles/389ds/defaults/main.yml @@ -0,0 +1,32 @@ +#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" +ds389_hostname: "{{ ds389_service_name }}" + +# Network configuration +ds389_backend_network: "backend" +ds389_ldap_port: 3389 +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/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..117f12b --- /dev/null +++ b/roles/389ds/tasks/main.yml @@ -0,0 +1,76 @@ +#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 + +- 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 new file mode 100644 index 0000000..7e0c7c0 --- /dev/null +++ b/roles/389ds/templates/docker-compose.yml.j2 @@ -0,0 +1,19 @@ +services: + {{ ds389_service_name }}: + image: {{ ds389_image }} + hostname: {{ ds389_hostname }} + 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 }}: 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 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/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..8942bf2 --- /dev/null +++ b/roles/authentik_outpost_ldap/defaults/main.yml @@ -0,0 +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/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..79a350a --- /dev/null +++ b/roles/authentik_outpost_ldap/tasks/main.yml @@ -0,0 +1,31 @@ +#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 + 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 %} 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 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..3cfb559 --- /dev/null +++ b/roles/collabora/defaults/main.yml @@ -0,0 +1,35 @@ +#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 }}" +collabora_docker_volume_dir: "{{ docker_volume_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 + +# 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" + +# 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/handlers/main.yml b/roles/collabora/handlers/main.yml new file mode 100644 index 0000000..bfd2b02 --- /dev/null +++ b/roles/collabora/handlers/main.yml @@ -0,0 +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/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..b6146c7 --- /dev/null +++ b/roles/collabora/tasks/main.yml @@ -0,0 +1,34 @@ +#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 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 + 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/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 new file mode 100644 index 0000000..c0f589e --- /dev/null +++ b/roles/collabora/templates/docker-compose.yml.j2 @@ -0,0 +1,34 @@ +services: + collabora: + image: {{ collabora_image }} + container_name: {{ collabora_service_name }} + restart: unless-stopped + environment: + 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: + - {{ 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/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 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..7b67976 --- /dev/null +++ b/roles/drawio/defaults/main.yml @@ -0,0 +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 new file mode 100644 index 0000000..f1ef0da --- /dev/null +++ b/roles/drawio/handlers/main.yml @@ -0,0 +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/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..67bd50d --- /dev/null +++ b/roles/drawio/tasks/main.yml @@ -0,0 +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/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 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 78b32ab..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 @@ -8,43 +9,67 @@ 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" 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: "" + +# 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 Admin-Gruppe (muss in Keycloak existieren) -oidc_admin_group: "homarr-admins" +# 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: "" +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 (override in inventory or via vault) homarr_admin_username: "admin" -homarr_admin_email: "admin@digitalboard.ch" -homarr_admin_password: "ChangeMe123!" \ No newline at end of file +homarr_admin_email: "admin@example.com" +homarr_admin_password: "ChangeMe123!" + +# Applications shown on the default board. +# 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). +# +# 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 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 diff --git a/roles/homarr/tasks/main.yml b/roles/homarr/tasks/main.yml index f8dd3df..06488f4 100644 --- a/roles/homarr/tasks/main.yml +++ b/roles/homarr/tasks/main.yml @@ -3,7 +3,43 @@ # 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-fA-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 + +- 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" + +- 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 # ===================================================================== - name: Ensure required packages are installed @@ -11,7 +47,6 @@ name: - sqlite3 - python3-docker - - python3-bcrypt state: present - name: Create docker compose directory @@ -33,11 +68,11 @@ - name: Check if database already exists ansible.builtin.stat: - path: "{{ homarr_db_dir }}" + path: "{{ homarr_db }}" register: db_exists # ===================================================================== -# 2. CONTAINER STARTEN +# 2. START CONTAINER # ===================================================================== - name: Create docker-compose file for homarr @@ -52,345 +87,62 @@ state: present # ===================================================================== -# 3. AUF DATENBANK WARTEN +# 3. WAIT FOR DATABASE # ===================================================================== - 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.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 }}" "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 }}" "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' - -- 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 + ansible.builtin.command: + cmd: sqlite3 "{{ homarr_db }}" + stdin: "{{ lookup('template', 'homarr_seed.sql.j2') }}" 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 diff --git a/roles/homarr/templates/docker-compose.yml.j2 b/roles/homarr/templates/docker-compose.yml.j2 index b953ed6..2d81063 100644 --- a/roles/homarr/templates/docker-compose.yml.j2 +++ b/roles/homarr/templates/docker-compose.yml.j2 @@ -1,9 +1,9 @@ #---------------------------------------------------------------------# -# Homarr - A simple, yet powerful dashboard for your server. # +# 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: @@ -16,28 +16,27 @@ 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: - 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 diff --git a/roles/homarr/templates/homarr_seed.sql.j2 b/roles/homarr/templates/homarr_seed.sql.j2 new file mode 100644 index 0000000..fdb1a2f --- /dev/null +++ b/roles/homarr/templates/homarr_seed.sql.j2 @@ -0,0 +1,211 @@ +{#- + 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; + +-- ===================================================================== +-- 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 (auto-generated from homarr_apps variable) +-- ===================================================================== + +{% if homarr_apps | length > 0 %} +{% for app in homarr_apps %} +INSERT OR IGNORE INTO app (id, name, description, icon_url, href) +VALUES ( + '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-{{ app.id }}', + 'board-default', + 'app', + '{"json": {"appId": "app-{{ app.id }}"}}', + '{"json": {}}' +); + +{% endfor %} + +INSERT OR REPLACE INTO item_layout (item_id, section_id, layout_id, x_offset, y_offset, width, height) +VALUES +{% 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 +{% 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 %} +; + +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 diff --git a/roles/keycloak/defaults/main.yml b/roles/keycloak/defaults/main.yml index 66d0a72..df29e65 100644 --- a/roles/keycloak/defaults/main.yml +++ b/roles/keycloak/defaults/main.yml @@ -33,3 +33,97 @@ keycloak_use_ssl: true 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 + +# 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 + +# 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 05db2ef..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: @@ -30,3 +32,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..f1d915a --- /dev/null +++ b/roles/keycloak/tasks/provisioning.yml @@ -0,0 +1,190 @@ +#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 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: + 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 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: + 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']) }}" + protocol_mappers: "{{ item.protocol_mappers | default(omit) }}" + 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..e08a2c7 100644 --- a/roles/keycloak/templates/docker-compose.yml.j2 +++ b/roles/keycloak/templates/docker-compose.yml.j2 @@ -32,13 +32,26 @@ 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" +{% 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: diff --git a/roles/nextcloud/defaults/main.yml b/roles/nextcloud/defaults/main.yml index 2e5a61e..7535b5a 100644 --- a/roles/nextcloud/defaults/main.yml +++ b/roles/nextcloud/defaults/main.yml @@ -14,6 +14,8 @@ 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" nextcloud_postgres_db: nextcloud @@ -26,10 +28,14 @@ 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 +# 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 @@ -48,6 +54,13 @@ 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 +nextcloud_notify_push_image: "icewind1991/notify_push:1.3.1" + # Non-default apps to install and enable nextcloud_apps_to_install: - groupfolders @@ -55,4 +68,55 @@ nextcloud_apps_to_install: - spreed - user_ldap - user_oidc - - whiteboard \ No newline at end of file + - 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) +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 + +# 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" +# 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 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/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/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 f15103c..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 @@ -55,9 +61,33 @@ - (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 nextcloud draw.io + 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 + +- 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/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/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/docker-compose.yml.j2 b/roles/nextcloud/templates/docker-compose.yml.j2 index b8a8a4d..9f15760 100644 --- a/roles/nextcloud/templates/docker-compose.yml.j2 +++ b/roles/nextcloud/templates/docker-compose.yml.j2 @@ -61,11 +61,14 @@ 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: - {{ nextcloud_backend_network }} +{% for net in nextcloud_extra_networks %} + - {{ net }} +{% endfor %} nextcloud: image: {{ nextcloud_image }} @@ -79,14 +82,14 @@ 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 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 }} @@ -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 %} @@ -109,35 +115,44 @@ services: {% endfor %} {% endif %} -{% if nextcloud_enable_collabora %} - collabora: - image: {{ nextcloud_collabora_image }} +{% if nextcloud_enable_notify_push %} + notify-push: + image: {{ nextcloud_notify_push_image }} restart: always + depends_on: + - redis + - db + volumes: + - {{ nextcloud_docker_volume_dir }}/nextcloud/:/var/www/html 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 + 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_collabora_service_name }}.rule=Host(`{{ nextcloud_collabora_domain }}`) - - traefik.http.services.{{ nextcloud_collabora_service_name }}.loadbalancer.server.port=9980 + - 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_collabora_service_name }}.entrypoints=websecure - - traefik.http.routers.{{ nextcloud_collabora_service_name }}.tls=true + - traefik.http.routers.{{ nextcloud_service_name }}-push.entrypoints=websecure + - traefik.http.routers.{{ nextcloud_service_name }}-push.tls=true {% else %} - - traefik.http.routers.{{ nextcloud_collabora_service_name }}.entrypoints=web + - 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 }}: - 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 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 }}, + ), +); 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..1e102bf --- /dev/null +++ b/roles/opencloud/defaults/main.yml @@ -0,0 +1,86 @@ +#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: [] +opencloud_extra_networks: [] + +# Traefik configuration +opencloud_traefik_network: "proxy" +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 + +# 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" + +# Collabora integration (set opencloud_collabora_domain to enable) +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 + +# 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" +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/handlers/main.yml b/roles/opencloud/handlers/main.yml new file mode 100644 index 0000000..95b6986 --- /dev/null +++ b/roles/opencloud/handlers/main.yml @@ -0,0 +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/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..358abb7 --- /dev/null +++ b/roles/opencloud/tasks/main.yml @@ -0,0 +1,88 @@ +#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 + owner: "1000" + group: "1000" + mode: '0750' + +- name: Create opencloud config directory + file: + path: "{{ opencloud_docker_volume_dir }}/config" + state: directory + 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 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" + 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: 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 + 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/csp-override.yaml.j2 b/roles/opencloud/templates/csp-override.yaml.j2 new file mode 100644 index 0000000..29afd38 --- /dev/null +++ b/roles/opencloud/templates/csp-override.yaml.j2 @@ -0,0 +1,20 @@ +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 %} +{% 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'" + - "'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 new file mode 100644 index 0000000..b731526 --- /dev/null +++ b/roles/opencloud/templates/docker-compose.yml.j2 @@ -0,0 +1,145 @@ +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"] + volumes: + - {{ 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 }}" +{% else %} + OC_URL: "http://{{ opencloud_domain }}" +{% endif %} + OC_INSECURE: "true" + OC_LOG_LEVEL: "{{ opencloud_log_level }}" + PROXY_TLS: "false" +{% 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 }}" +{% 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 }}" +{% 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 %} +{% 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 %} +{% 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 }}" + 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 }} +{% 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 %} + - "{{ 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 }} +{% 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 }}: + external: true +{% for net in opencloud_extra_networks %} + {{ net }}: + external: true +{% endfor %} \ No newline at end of file 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 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