Nothing was group-restricted, so customers saw Dashboard, Photos, Office and the rest, plus Nextcloud's first-run and promo apps. - Disable for everyone: firstrunwizard, recommendations, related_resources, weather_status, survey_client, support, app_api, contactsinteraction, photos. A refused disable now fails the play (occ exits 0 on "can't be disabled"). - Restrict dashboard and office to staff. defaultapp=dashboard,files so staff land on the dashboard and customers fall through to Files. - libresign groups_request_sign pinned to staff and asserted in verify. LibreSign itself is deliberately NOT group-restricted: that also blocks anonymous requests and would break public signing links. - profile.enabled=false; lookup_server="" (lookup_server_connector can't be disabled). - README: what customers can open. Checked as a probe customer: apps=files,activity,libresign,text,viewer, lands in Files, can't request signatures; staff land on Dashboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
856 lines
35 KiB
YAML
856 lines
35 KiB
YAML
---
|
|
# de Byl Technologies Nextcloud (cloud.debyltech.com).
|
|
#
|
|
# Cloned from containers/skudak/cloud.yml, which carries the full reasoning for
|
|
# nearly every task below -- read the matching comment there before changing
|
|
# one here. Comments in this file cover only where the two instances differ.
|
|
#
|
|
# Differences from Skudak, by design:
|
|
# - Fresh install: NEXTCLOUD_ADMIN_* makes the first deploy install
|
|
# unattended, and LibreSign is installed from the app store rather than
|
|
# assumed present.
|
|
# - No Group Folders. Registration stays off, matching Skudak's live state:
|
|
# every account, staff and customer alike, is created by the admin, with
|
|
# customers in per-customer groups.
|
|
# - Outbound mail is AWS SES SMTP (noreply@debyltech.com), set here via occ
|
|
# so it lives in git rather than only in the admin UI.
|
|
# - Backups go to personal iDrive e2 via TrueNAS -- see the backup include
|
|
# at the bottom.
|
|
- name: create required debyltech cloud volumes
|
|
become: true
|
|
ansible.builtin.file:
|
|
path: "{{ item }}"
|
|
state: directory
|
|
owner: "{{ podman_subuid.stdout }}"
|
|
group: "{{ podman_subuid.stdout }}"
|
|
mode: 0755
|
|
notify: restorecon podman
|
|
loop:
|
|
- "{{ cloud_debyltech_path }}/apps"
|
|
- "{{ cloud_debyltech_path }}/config"
|
|
- "{{ cloud_debyltech_path }}/data"
|
|
- "{{ cloud_debyltech_path }}/mysql"
|
|
- "{{ cloud_debyltech_path }}/scripts"
|
|
- "{{ cloud_debyltech_path }}/redis"
|
|
|
|
- name: unshare chown the debyltech cloud volumes
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
changed_when: false
|
|
ansible.builtin.command: |
|
|
podman unshare chown -R 33:33 {{ cloud_debyltech_path }}/apps {{ cloud_debyltech_path }}/data {{ cloud_debyltech_path }}/config
|
|
|
|
- name: flush handlers
|
|
ansible.builtin.meta: flush_handlers
|
|
|
|
- import_tasks: podman/podman-check.yml
|
|
vars:
|
|
container_name: debyltech-cloud-db
|
|
container_image: "{{ db_image }}"
|
|
|
|
- name: create debyltech-cloud-db container
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
containers.podman.podman_container:
|
|
name: debyltech-cloud-db
|
|
image: "{{ db_image }}"
|
|
restart_policy: on-failure:3
|
|
log_driver: journald
|
|
network:
|
|
- shared
|
|
env:
|
|
MYSQL_ROOT_PASSWORD: "{{ cloud_debyltech_db_root_pass }}"
|
|
MYSQL_DATABASE: dtcloud
|
|
MYSQL_PASSWORD: "{{ cloud_debyltech_db_pass }}"
|
|
MYSQL_USER: dtcloud
|
|
volumes:
|
|
- "{{ cloud_debyltech_path }}/mysql:/var/lib/mysql"
|
|
|
|
- name: create systemd startup job for debyltech-cloud-db
|
|
include_tasks: podman/systemd-generate.yml
|
|
vars:
|
|
container_name: debyltech-cloud-db
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Redis: distributed cache + file locking. MUST exist before debyltech-cloud
|
|
# below -- see the Skudak equivalent for why.
|
|
- name: template debyltech cloud redis config
|
|
become: true
|
|
ansible.builtin.template:
|
|
src: nextcloud/redis-debyltech.conf.j2
|
|
dest: "{{ cloud_debyltech_path }}/redis/redis.conf"
|
|
owner: "{{ podman_subuid.stdout }}"
|
|
group: "{{ podman_subuid.stdout }}"
|
|
mode: 0640
|
|
notify: restorecon podman
|
|
no_log: true
|
|
|
|
- name: flush handlers
|
|
ansible.builtin.meta: flush_handlers
|
|
|
|
- name: unshare chown the debyltech redis config to the redis uid
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
changed_when: false
|
|
ansible.builtin.command: >
|
|
podman unshare chown 999:1000 {{ cloud_debyltech_path }}/redis/redis.conf
|
|
|
|
- import_tasks: podman/podman-check.yml
|
|
vars:
|
|
container_name: debyltech-cloud-redis
|
|
container_image: "{{ redis_image }}"
|
|
|
|
- name: create debyltech-cloud-redis container
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
containers.podman.podman_container:
|
|
name: debyltech-cloud-redis
|
|
image: "{{ redis_image }}"
|
|
restart_policy: on-failure:3
|
|
log_driver: journald
|
|
network:
|
|
- shared
|
|
volumes:
|
|
- "{{ cloud_debyltech_path }}/redis/redis.conf:/etc/redis/redis.conf:ro"
|
|
command: redis-server /etc/redis/redis.conf
|
|
|
|
- name: create systemd startup job for debyltech-cloud-redis
|
|
include_tasks: podman/systemd-generate.yml
|
|
vars:
|
|
container_name: debyltech-cloud-redis
|
|
|
|
- import_tasks: podman/podman-check.yml
|
|
vars:
|
|
container_name: debyltech-cloud
|
|
container_image: "{{ image }}"
|
|
|
|
- name: create debyltech cloud container
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
containers.podman.podman_container:
|
|
name: debyltech-cloud
|
|
image: "{{ image }}"
|
|
restart_policy: on-failure:3
|
|
log_driver: journald
|
|
network:
|
|
- shared
|
|
env:
|
|
MYSQL_PASSWORD: "{{ cloud_debyltech_db_pass }}"
|
|
MYSQL_DATABASE: dtcloud
|
|
MYSQL_HOST: debyltech-cloud-db
|
|
MYSQL_USER: dtcloud
|
|
# Read by the entrypoint ONLY on first start against an empty config
|
|
# volume, to run the install unattended; ignored on every start after.
|
|
NEXTCLOUD_ADMIN_USER: admin
|
|
NEXTCLOUD_ADMIN_PASSWORD: "{{ cloud_debyltech_admin_pass }}"
|
|
NEXTCLOUD_TRUSTED_DOMAINS: "{{ cloud_debyltech_server_name }}"
|
|
PHP_MEMORY_LIMIT: 1024M
|
|
PHP_UPLOAD_LIMIT: 512M
|
|
LC_ALL: C.UTF-8
|
|
LANG: C.UTF-8
|
|
REDIS_HOST: debyltech-cloud-redis
|
|
REDIS_HOST_PORT: "6379"
|
|
REDIS_HOST_PASSWORD: "{{ cloud_debyltech_redis_pass }}"
|
|
volumes:
|
|
- "{{ cloud_debyltech_path }}/apps:/var/www/html/custom_apps"
|
|
- "{{ cloud_debyltech_path }}/data:/var/www/html/data"
|
|
- "{{ cloud_debyltech_path }}/config:/var/www/html/config"
|
|
ports:
|
|
- "8091:80"
|
|
|
|
- name: create systemd startup job for debyltech-cloud
|
|
include_tasks: podman/systemd-generate.yml
|
|
vars:
|
|
container_name: debyltech-cloud
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# LibreSign
|
|
- name: install libresign runtime dependencies in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command:
|
|
cmd: >
|
|
podman exec -u 0 debyltech-cloud
|
|
sh -c "apt-get update && apt-get install -y --no-install-recommends
|
|
poppler-utils ghostscript && rm -rf /var/lib/apt/lists/*"
|
|
register: libresign_deps
|
|
changed_when: "'is already the newest version' not in libresign_deps.stdout"
|
|
|
|
# On the FIRST deploy this also waits out the unattended install, which takes
|
|
# noticeably longer than a restart -- hence the larger budget than Skudak's.
|
|
- name: wait for nextcloud to be ready in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ status --output=json
|
|
register: debyltech_occ_ready
|
|
# String match, not from_json: before the install finishes occ can print a
|
|
# plain-text warning ahead of the JSON, and a parse error would abort the
|
|
# retry loop instead of waiting. Skudak's bare 'installed' check would also
|
|
# match "installed":false, which is exactly the state being waited out here.
|
|
until: >-
|
|
debyltech_occ_ready.rc == 0
|
|
and '"installed":true' in debyltech_occ_ready.stdout
|
|
retries: 60
|
|
delay: 5
|
|
changed_when: false
|
|
|
|
# LibreSign is PINNED (libresign_version / libresign_sha256 in tasks/main.yml)
|
|
# and installed from the upstream GitHub release, NOT `occ app:install`, which
|
|
# always takes whatever the app store has that day. On 2026-09-28 that was a
|
|
# same-day 14.2.3 whose tarball shipped without appinfo/install-*.json -- the
|
|
# maintainer-signed metadata LibreSign verifies its java/pdftk/jsignpdf
|
|
# downloads against -- so configure:check failed all three on a clean install.
|
|
#
|
|
# Upgrading: bump both pins together (the sha256 is on the GitHub release
|
|
# asset) and deploy; the tree is replaced and `occ upgrade` runs the app's
|
|
# migrations. Downgrading is refused below: Nextcloud does not support it, and
|
|
# the only way back is removing the app, which discards its config and CA.
|
|
- name: read installed libresign version in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ config:app:get libresign installed_version
|
|
register: libresign_installed
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: refuse to downgrade libresign in debyltech-cloud
|
|
ansible.builtin.fail:
|
|
msg: >-
|
|
LibreSign {{ libresign_installed.stdout }} is installed but the pin is
|
|
{{ libresign_version }}. Nextcloud cannot downgrade an app in place --
|
|
raise the pin, or remove the app deliberately if nothing has been signed.
|
|
when:
|
|
- libresign_installed.rc == 0
|
|
- libresign_installed.stdout is version(libresign_version, '>')
|
|
|
|
- name: install pinned libresign release in debyltech-cloud
|
|
when: libresign_installed.rc != 0 or libresign_installed.stdout != libresign_version
|
|
block:
|
|
- name: fetch pinned libresign release
|
|
become: true
|
|
ansible.builtin.get_url:
|
|
url: "https://github.com/LibreSign/libresign/releases/download/v{{ libresign_version }}/libresign-v{{ libresign_version }}.tar.gz"
|
|
dest: "{{ cloud_debyltech_path }}/scripts/libresign-v{{ libresign_version }}.tar.gz"
|
|
checksum: "sha256:{{ libresign_sha256 }}"
|
|
mode: 0644
|
|
|
|
- name: remove previous libresign app tree
|
|
become: true
|
|
ansible.builtin.file:
|
|
path: "{{ cloud_debyltech_path }}/apps/libresign"
|
|
state: absent
|
|
|
|
- name: unpack pinned libresign release into custom_apps
|
|
become: true
|
|
ansible.builtin.unarchive:
|
|
src: "{{ cloud_debyltech_path }}/scripts/libresign-v{{ libresign_version }}.tar.gz"
|
|
dest: "{{ cloud_debyltech_path }}/apps/"
|
|
remote_src: true
|
|
# Unpacked as root the files keep the tarball's owners, which lie
|
|
# outside the podman user's subuid range, so the unshare chown below
|
|
# is refused. Same two-step as the debyltechmail copy.
|
|
owner: "{{ podman_subuid.stdout }}"
|
|
group: "{{ podman_subuid.stdout }}"
|
|
notify: restorecon podman
|
|
|
|
- name: unshare chown the libresign app tree
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
changed_when: false
|
|
ansible.builtin.command: >
|
|
podman unshare chown -R 33:33 {{ cloud_debyltech_path }}/apps/libresign
|
|
|
|
- name: flush handlers
|
|
ansible.builtin.meta: flush_handlers
|
|
|
|
# Only an in-place upgrade needs this; a first install is handled by the
|
|
# app:enable below.
|
|
- name: run libresign migrations in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud php occ upgrade
|
|
when: libresign_installed.rc == 0
|
|
|
|
- name: ensure libresign app is enabled in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ app:enable libresign
|
|
register: libresign_enable
|
|
changed_when: "'already enabled' not in libresign_enable.stdout"
|
|
|
|
# 14.2.x's downloader does not create its own target directories: on a fresh
|
|
# appdata every java/pdftk download fails with "Directory ... does not exist
|
|
# for sink value". Creating them first is harmless once they exist.
|
|
- name: pre-create libresign binary directories in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
changed_when: false
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud sh -c
|
|
'd=$(ls -d /var/www/html/data/appdata_*/libresign) &&
|
|
mkdir -p "$d/x86_64/linux/java" "$d/x86_64/pdftk"'
|
|
|
|
# "Finished with success" is printed even when every download failed, so this
|
|
# check only catches the command itself falling over. The real gate is the
|
|
# configure:check verify task below, which hashes each binary against the
|
|
# release's signed metadata.
|
|
- name: install libresign java/pdftk/jsignpdf binaries in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ libresign:install --java --pdftk --jsignpdf
|
|
register: libresign_install
|
|
changed_when: false
|
|
failed_when: "'Finished with success' not in libresign_install.stdout"
|
|
|
|
- name: check whether libresign root certificate is configured
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ libresign:configure:check --certificate
|
|
register: libresign_cert_check
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
# Guarded: re-running would mint a new root CA and orphan every certificate
|
|
# already issued. No --ou -- see skudak/cloud.yml.
|
|
- name: generate libresign root certificate for debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ libresign:configure:openssl
|
|
--cn="{{ libresign_debyltech_cert_cn }}"
|
|
-o "{{ libresign_debyltech_cert_o }}"
|
|
-c "{{ libresign_debyltech_cert_c }}"
|
|
-s "{{ libresign_debyltech_cert_st }}"
|
|
-l "{{ libresign_debyltech_cert_l }}"
|
|
when: "'error' in libresign_cert_check.stdout"
|
|
changed_when: true
|
|
|
|
# Signers are mostly customers WITHOUT an account, reached by emailed
|
|
# invitation; the ID-document gate would leave them unable to sign at all.
|
|
- name: relax libresign identification-document gate in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ config:app:set libresign identification_documents --value=0
|
|
register: libresign_ident
|
|
changed_when: "'is now set to' in libresign_ident.stdout"
|
|
|
|
# Must be exactly GRAPHIC_ONLY -- see skudak/cloud.yml.
|
|
- name: use signature-only stamp in debyltech-cloud libresign
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ config:app:set libresign signature_render_mode --value=GRAPHIC_ONLY
|
|
register: libresign_render
|
|
changed_when: "'is now set to' in libresign_render.stdout"
|
|
|
|
# Lets account-owned emails be added as signers. NEVER set the _email variant
|
|
# of this key to 'no' -- see skudak/cloud.yml.
|
|
- name: allow account-owned emails as libresign signers in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ config:app:set core
|
|
shareapi_restrict_user_enumeration_full_match --value=no
|
|
register: debyltech_enum_fullmatch
|
|
changed_when: "'is now set to' in debyltech_enum_fullmatch.stdout"
|
|
|
|
# Settings Skudak only ever had from clicks in the LibreSign admin page, never
|
|
# in git -- a fresh instance without them cannot invite anyone by address:
|
|
#
|
|
# identify_methods The EMAIL identification method. Without it the signer
|
|
# search only lists accounts, so an outside address
|
|
# returns a bare "No signers." -- the whole point of this
|
|
# instance. clickToSign (no emailed code) and
|
|
# can_create_account=false, as on Skudak: the emailed link
|
|
# is the identity check, and customers never get accounts.
|
|
# signature_background_type=deleted
|
|
# Drops the LibreSign logo watermark from behind the
|
|
# stamp, so with GRAPHIC_ONLY the stamp is the drawn mark
|
|
# and nothing else.
|
|
# collect_metadata Records signer IP/user agent alongside each signature.
|
|
- name: set libresign signer identification and stamp settings in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ config:app:set libresign {{ item.k }} --value={{ item.v | quote }}
|
|
register: debyltech_libresign_settings
|
|
changed_when: "'is now set to' in debyltech_libresign_settings.stdout"
|
|
loop:
|
|
- k: identify_methods
|
|
v: >-
|
|
{{ [{'name': 'email', 'friendly_name': 'Email', 'enabled': true,
|
|
'mandatory': true,
|
|
'signatureMethods': {
|
|
'clickToSign': {'name': 'clickToSign', 'enabled': true},
|
|
'emailToken': {'name': 'emailToken', 'enabled': false}},
|
|
'can_create_account': false,
|
|
'test_url': '/index.php/settings/admin/mailtest',
|
|
'signatureMethodEnabled': 'clickToSign'}] | to_json }}
|
|
- {k: signature_background_type, v: deleted}
|
|
- {k: collect_metadata, v: "1"}
|
|
loop_control:
|
|
label: "{{ item.k }}"
|
|
|
|
# The validation footer ("Digitally signed by ... Validate in <url>") is WANTED
|
|
# -- only its QR code goes, below. FooterHandler defaults add_footer to true
|
|
# when unset, but the 14.2 admin page renders unset as UNCHECKED, inviting
|
|
# someone to "correct" it into actually turning the footer off. Stored
|
|
# explicitly so the page shows what the code does.
|
|
- name: keep libresign validation footer text in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ config:app:set libresign add_footer --value=1 --type=boolean
|
|
register: libresign_footer
|
|
changed_when: "'is now set to' in libresign_footer.stdout"
|
|
|
|
- name: drop libresign validation QR code from signed-PDF footer in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ config:app:set libresign write_qrcode_on_footer
|
|
--value=0 --type=boolean
|
|
register: libresign_qr
|
|
changed_when: "'is now set to' in libresign_qr.stdout"
|
|
|
|
- name: verify libresign configuration in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ libresign:configure:check
|
|
register: libresign_verify
|
|
changed_when: false
|
|
# Double backslashes: Jinja unescapes string literals, so a single '\b'
|
|
# becomes a BACKSPACE character and this could never match -- which is
|
|
# how a check reporting three errors passed clean on 2026-09-28.
|
|
failed_when: libresign_verify.stdout is search('\\berror\\b')
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background jobs. A fresh install defaults to AJAX mode, which only runs jobs
|
|
# while someone has the web UI open -- LibreSign's queued signature mail and
|
|
# every cleanup job would stall. The cloud-cron timer included below drives
|
|
# cron.php; this tells Nextcloud to expect it.
|
|
- name: set debyltech-cloud background jobs to cron
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ config:app:set core backgroundjobs_mode --value=cron
|
|
register: debyltech_bgjobs
|
|
changed_when: "'is now set to' in debyltech_bgjobs.stdout"
|
|
|
|
- name: disable nextcloud signup link in debyltech-cloud config
|
|
become: true
|
|
ansible.builtin.lineinfile:
|
|
path: "{{ cloud_debyltech_path }}/config/config.php"
|
|
regexp: "^\\s*'simpleSignUpLink\\.shown'\\s*=>"
|
|
line: " 'simpleSignUpLink.shown' => false,"
|
|
insertbefore: '^\);'
|
|
create: false
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Customer isolation. Customers are admin-created accounts in a per-customer
|
|
# group, and must only READ what staff share with them -- not upload, not
|
|
# share onward, and not discover that other customers exist. Verified
|
|
# 2026-09-28 against a test customer, both in the UI and with the sharee and
|
|
# contacts-menu search services run as that user.
|
|
#
|
|
# shareapi_exclude_groups=allow + list=[staff]
|
|
# Only staff may share. NOT "yes" (exclude mode): that only disables
|
|
# sharing for users whose groups are ALL excluded, so a customer in
|
|
# their own per-customer group would never be caught by it.
|
|
# shareapi_allow_share_dialog_user_enumeration=no
|
|
# No partial-match browsing of accounts or groups, for anyone. By
|
|
# default a customer typing "bas" found the owner's account. Staff
|
|
# share to a customer group by typing its exact name; LibreSign signers
|
|
# are found by email and are unaffected.
|
|
# shareapi_default_permissions=1
|
|
# New shares default to View only; tick "Allow editing" per share to
|
|
# let a customer upload.
|
|
# files default_quota=0 B
|
|
# No personal storage, so no "+ New" in a customer's own home. Uploads
|
|
# into a share granted editing count against the OWNER's quota and still
|
|
# work. Staff are exempted by the next task.
|
|
# dav enableDefaultContact=false
|
|
# No "Leon Green" sample contact in new address books.
|
|
- name: set debyltech-cloud customer isolation policy
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ config:app:set {{ item.app }} {{ item.k }} --value={{ item.v | quote }}
|
|
{{ ('--type=' ~ item.t) if item.t is defined else '' }}
|
|
register: debyltech_isolation
|
|
changed_when: "'is now set to' in debyltech_isolation.stdout"
|
|
loop:
|
|
- {app: core, k: shareapi_exclude_groups, v: allow}
|
|
- {app: core, k: shareapi_exclude_groups_list, v: "{{ [cloud_debyltech_staff_group] | to_json }}"}
|
|
- {app: core, k: shareapi_allow_share_dialog_user_enumeration, v: "no"}
|
|
- {app: core, k: shareapi_default_permissions, v: "1"}
|
|
- {app: files, k: default_quota, v: "0 B"}
|
|
- {app: dav, k: enableDefaultContact, v: "0", t: boolean}
|
|
loop_control:
|
|
label: "{{ item.app }}.{{ item.k }}"
|
|
|
|
- name: exempt debyltech-cloud staff from the zero default quota
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.shell: |
|
|
set -o pipefail
|
|
occ() { podman exec -u www-data debyltech-cloud php occ "$@"; }
|
|
occ user:info {{ item | quote }} >/dev/null 2>&1 || exit 0
|
|
cur=$(occ user:setting {{ item | quote }} files quota) || cur='<unset>'
|
|
if [ "$cur" != none ]; then
|
|
occ user:setting {{ item | quote }} files quota none
|
|
echo CHANGED
|
|
fi
|
|
args:
|
|
executable: /bin/bash
|
|
register: debyltech_staff_quota
|
|
changed_when: "'CHANGED' in debyltech_staff_quota.stdout"
|
|
loop: "{{ cloud_debyltech_staff_users }}"
|
|
|
|
# What a customer can reach. Nextcloud had nothing group-restricted, so every
|
|
# customer saw Dashboard, Photos, Office and the rest in the app menu.
|
|
#
|
|
# Disabled outright -- promos, prompts, or directory-ish features a business
|
|
# file-and-signing portal has no use for (contactsinteraction silently adds
|
|
# whoever shares with you to your address book; app_api only produces a
|
|
# setup warning here). lookup_server_connector cannot be disabled (occ refuses)
|
|
# -- the empty `lookup_server` system value below switches it off instead.
|
|
#
|
|
# Restricted to staff: dashboard and office. `defaultapp` below lists
|
|
# dashboard first so staff land there and customers fall through to Files.
|
|
#
|
|
# NOT restricted: libresign. A group restriction is enforced for anonymous
|
|
# requests too (AppManager::checkAppForUser returns false for no user), so it
|
|
# would break the public signing links sent to outside signers and to any
|
|
# customer already logged in. Who may AUTHOR requests is LibreSign's own
|
|
# groups_request_sign, pinned to staff below.
|
|
- name: disable unneeded apps in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.shell: |
|
|
set -o pipefail
|
|
occ() { podman exec -u www-data debyltech-cloud php occ "$@"; }
|
|
if occ app:list --output=json | python3 -c 'import json,sys; sys.exit(0 if sys.argv[1] in json.load(sys.stdin)["enabled"] else 1)' {{ item | quote }}; then
|
|
occ app:disable {{ item | quote }}
|
|
echo CHANGED
|
|
fi
|
|
args:
|
|
executable: /bin/bash
|
|
register: debyltech_app_disable
|
|
changed_when: "'CHANGED' in debyltech_app_disable.stdout"
|
|
# occ exits 0 even when it refuses ("can't be disabled").
|
|
failed_when: debyltech_app_disable.rc != 0 or "can't be disabled" in debyltech_app_disable.stdout
|
|
loop:
|
|
- firstrunwizard
|
|
- recommendations
|
|
- related_resources
|
|
- weather_status
|
|
- survey_client
|
|
- support
|
|
- app_api
|
|
- contactsinteraction
|
|
- photos
|
|
|
|
- name: restrict staff-only apps in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.shell: |
|
|
set -o pipefail
|
|
occ() { podman exec -u www-data debyltech-cloud php occ "$@"; }
|
|
want={{ [cloud_debyltech_staff_group] | to_json | quote }}
|
|
if [ "$(occ config:app:get {{ item | quote }} enabled || true)" != "$want" ]; then
|
|
occ app:enable --groups {{ cloud_debyltech_staff_group | quote }} {{ item | quote }} >/dev/null
|
|
echo CHANGED
|
|
fi
|
|
args:
|
|
executable: /bin/bash
|
|
register: debyltech_app_restrict
|
|
changed_when: "'CHANGED' in debyltech_app_restrict.stdout"
|
|
loop:
|
|
- dashboard
|
|
- office
|
|
|
|
- name: pin who may request libresign signatures in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ config:app:set libresign groups_request_sign
|
|
--value={{ [cloud_debyltech_staff_group] | to_json | quote }}
|
|
register: debyltech_request_sign
|
|
changed_when: "'is now set to' in debyltech_request_sign.stdout"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Branding: debyltechmail, cloned from skudakmail. custom_apps is a persisted
|
|
# bind mount, so only enabling and config need reasserting.
|
|
# Owned directly by the HOST uid that rootless podman maps www-data (33) to --
|
|
# subuid start + 32, since container uid 1 is the first subuid. Skudak copies
|
|
# as the subuid and then `podman unshare chown`s, which flips ownership back
|
|
# and forth so the copy reports changed on every run; here that would also
|
|
# re-import the theming logos below every time.
|
|
- name: deploy debyltechmail email-template app to debyltech-cloud
|
|
become: true
|
|
ansible.builtin.copy:
|
|
src: debyltechmail/
|
|
dest: "{{ cloud_debyltech_path }}/apps/debyltechmail/"
|
|
owner: "{{ podman_subuid.stdout | int + 32 }}"
|
|
group: "{{ podman_subuid.stdout | int + 32 }}"
|
|
mode: 0644
|
|
directory_mode: 0755
|
|
register: debyltechmail_copy
|
|
notify: restorecon podman
|
|
|
|
- name: enable debyltechmail app in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud php occ app:enable debyltechmail
|
|
register: debyltechmail_enable
|
|
changed_when: "'already enabled' not in debyltechmail_enable.stdout"
|
|
|
|
# Behind rootless podman's port forwarder, every request -- Caddy's included --
|
|
# reaches Apache FROM THE CONTAINER'S OWN ADDRESS on `shared`, not from the
|
|
# host. Unless exactly that address is a trusted proxy, Nextcloud ignores the
|
|
# X-Forwarded-For header Caddy sends, so every client looks like one IP:
|
|
# brute-force throttling then penalises everyone at once. Read per deploy
|
|
# because the address is assigned at container creation, and deploys are the
|
|
# only thing that recreate it.
|
|
- name: read debyltech-cloud container address
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman inspect debyltech-cloud
|
|
--format "{{ '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' }}"
|
|
register: debyltech_cloud_ip
|
|
changed_when: false
|
|
failed_when: debyltech_cloud_ip.stdout is not match('^[0-9.]+$')
|
|
|
|
# System config, set only when it differs so a clean re-deploy reports no
|
|
# changes (Skudak's equivalents report changed on every run). Values are
|
|
# single-quoted into the shell, so the backslashes in mail_template_class pass
|
|
# through literally. overwrite.cli.url is what LibreSign invitation links and
|
|
# mail asset URLs are built from when sent by a background job.
|
|
- name: set debyltech-cloud system config
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.shell: |
|
|
set -o pipefail
|
|
occ() { podman exec -u www-data debyltech-cloud php occ "$@"; }
|
|
# An UNSET key prints nothing and exits 1 -- indistinguishable from ""
|
|
# by output alone, which would skip setting an intentionally empty value.
|
|
cur=$(occ config:system:get {{ item.k }}) || cur='<unset>'
|
|
if [ "$cur" != {{ item.v | quote }} ]; then
|
|
occ config:system:set {{ item.k }} --value={{ item.v | quote }} --type={{ item.t | default('string') }} >/dev/null
|
|
echo CHANGED
|
|
fi
|
|
args:
|
|
executable: /bin/bash
|
|
register: debyltech_sysconfig
|
|
changed_when: "'CHANGED' in debyltech_sysconfig.stdout"
|
|
loop:
|
|
- {k: overwrite.cli.url, v: "https://{{ cloud_debyltech_server_name }}"}
|
|
- {k: overwriteprotocol, v: https}
|
|
- {k: trusted_proxies 0, v: "{{ debyltech_cloud_ip.stdout }}"}
|
|
# Hour in UTC: 05:00 UTC is 01:00/00:00 Eastern, ahead of the 04:15 backup.
|
|
- {k: maintenance_window_start, v: "5", t: integer}
|
|
- {k: default_phone_region, v: US}
|
|
# New accounts -- customers above all -- start with an empty home rather
|
|
# than Nextcloud's sample Manual/intro video/Readme and Templates folder.
|
|
- {k: skeletondirectory, v: ""}
|
|
- {k: templatedirectory, v: ""}
|
|
# First ENABLED app wins: staff get the dashboard, and customers -- who
|
|
# cannot open it (restricted below) -- fall through to Files.
|
|
- {k: defaultapp, v: "dashboard,files"}
|
|
# No per-account profile pages.
|
|
- {k: profile.enabled, v: "false", t: boolean}
|
|
# Never query or publish to the global lookup server (lookup.nextcloud.com).
|
|
- {k: lookup_server, v: ""}
|
|
- {k: loglevel, v: "2", t: integer}
|
|
- {k: log_rotate_size, v: "10485760", t: integer}
|
|
- {k: mail_template_class, v: "OCA\\Debyltechmail\\Mail\\DebyltechEMailTemplate"}
|
|
- {k: mail_smtpmode, v: smtp}
|
|
- {k: mail_smtphost, v: "{{ cloud_debyltech_smtp_host }}"}
|
|
- {k: mail_smtpport, v: "{{ cloud_debyltech_smtp_port }}", t: integer}
|
|
- {k: mail_smtpsecure, v: ssl}
|
|
- {k: mail_smtpauth, v: "true", t: boolean}
|
|
- {k: mail_from_address, v: noreply}
|
|
- {k: mail_domain, v: debyltech.com}
|
|
loop_control:
|
|
label: "{{ item.k }}"
|
|
|
|
- name: set debyltech-cloud SES SMTP credentials
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.shell: |
|
|
set -o pipefail
|
|
occ() { podman exec -u www-data debyltech-cloud php occ "$@"; }
|
|
cur=$(occ config:system:get {{ item.k }} || true)
|
|
if [ "$cur" != {{ item.v | quote }} ]; then
|
|
occ config:system:set {{ item.k }} --value={{ item.v | quote }} >/dev/null
|
|
echo CHANGED
|
|
fi
|
|
args:
|
|
executable: /bin/bash
|
|
register: debyltech_smtp_creds
|
|
changed_when: "'CHANGED' in debyltech_smtp_creds.stdout"
|
|
loop:
|
|
- {k: mail_smtpname, v: "{{ cloud_debyltech_smtp_user }}"}
|
|
- {k: mail_smtppassword, v: "{{ cloud_debyltech_smtp_pass }}"}
|
|
loop_control:
|
|
label: "{{ item.k }}"
|
|
no_log: true
|
|
|
|
# Compared first: theming:config prints "Updated" even when nothing changed.
|
|
- name: set debyltech-cloud theming
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.shell: |
|
|
set -o pipefail
|
|
occ() { podman exec -u www-data debyltech-cloud php occ "$@"; }
|
|
cur=$(occ config:app:get theming {{ item.k }} || true)
|
|
if [ "$cur" != {{ item.v | quote }} ]; then
|
|
occ theming:config {{ item.k }} {{ item.v | quote }} >/dev/null
|
|
echo CHANGED
|
|
fi
|
|
args:
|
|
executable: /bin/bash
|
|
loop:
|
|
- {k: name, v: "de Byl Technologies"}
|
|
- {k: slogan, v: "Hardware, firmware and design services"}
|
|
- {k: url, v: "https://debyltech.com"}
|
|
- {k: primary_color, v: "{{ theming_debyltech_primary }}"}
|
|
- {k: background_color, v: "{{ theming_debyltech_background }}"}
|
|
register: debyltech_theming
|
|
changed_when: "'CHANGED' in debyltech_theming.stdout"
|
|
loop_control:
|
|
label: "{{ item.k }}"
|
|
|
|
# The web UI logos ship inside the debyltechmail app (the white variants; the
|
|
# ink wordmark is the mail one). theming:config re-imports the file on every
|
|
# call, so it runs only when the app's files changed or no logo is set yet.
|
|
# `logo` is the wide wordmark on the login page; `logoheader` is the square
|
|
# mark in the top bar, where a wordmark would shrink to illegibility.
|
|
# Plain theming_debyltech_background instead of Nextcloud's stock blue-shapes
|
|
# image. `background backgroundColor` is a special case in UpdateConfig.php
|
|
# (absent from --help) that drops the image and sets backgroundMime, exactly
|
|
# what the admin UI's "remove background image" does.
|
|
- name: use a plain colour login background in debyltech-cloud
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.shell: |
|
|
set -o pipefail
|
|
occ() { podman exec -u www-data debyltech-cloud php occ "$@"; }
|
|
if [ "$(occ config:app:get theming backgroundMime || true)" != backgroundColor ]; then
|
|
occ theming:config background backgroundColor >/dev/null
|
|
echo CHANGED
|
|
fi
|
|
args:
|
|
executable: /bin/bash
|
|
register: debyltech_background
|
|
changed_when: "'CHANGED' in debyltech_background.stdout"
|
|
|
|
- name: check debyltech-cloud theming logos
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud
|
|
php occ config:app:get theming {{ item }}Mime
|
|
loop: [logo, logoheader]
|
|
register: debyltech_logo_mime
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: set debyltech-cloud theming logos
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.command: >
|
|
podman exec -u www-data debyltech-cloud php occ theming:config {{ item.item }}
|
|
/var/www/html/custom_apps/debyltechmail/img/{{ logo_files[item.item] }}
|
|
loop: "{{ debyltech_logo_mime.results }}"
|
|
when: debyltechmail_copy is changed or item.rc != 0 or item.stdout == ''
|
|
vars:
|
|
logo_files:
|
|
logo: debyltech-wordmark-white.png
|
|
logoheader: debyltech-mark-white.png
|
|
loop_control:
|
|
label: "{{ item.item }}"
|
|
|
|
# Fails the play if branding, the LibreSign settings above, or Redis locking
|
|
# have silently regressed -- see skudak/cloud.yml.
|
|
- name: template debyltechmail verification script
|
|
become: true
|
|
ansible.builtin.template:
|
|
src: nextcloud/debyltechmail-verify.php.j2
|
|
dest: "{{ cloud_debyltech_path }}/scripts/debyltechmail-verify.php"
|
|
owner: "{{ podman_subuid.stdout }}"
|
|
group: "{{ podman_subuid.stdout }}"
|
|
mode: 0644
|
|
notify: restorecon podman
|
|
|
|
- name: verify debyltech mail branding is live
|
|
become: true
|
|
become_user: "{{ podman_user }}"
|
|
ansible.builtin.shell: >
|
|
set -o pipefail;
|
|
podman exec -i -u www-data debyltech-cloud php
|
|
< {{ cloud_debyltech_path }}/scripts/debyltechmail-verify.php
|
|
args:
|
|
executable: /bin/bash
|
|
register: debyltechmail_verify
|
|
changed_when: false
|
|
|
|
- include_tasks: containers/cloud-cron.yml
|
|
vars:
|
|
cron_name: debyltech-cloud
|
|
cron_container: debyltech-cloud
|
|
cron_script_path: /usr/local/bin/debyltech-cloud-cron.sh
|
|
|
|
# BUSINESS data that DELIBERATELY reaches personal storage -- the opposite of
|
|
# Skudak, and on purpose: de Byl Technologies LLC is the owner's own company.
|
|
#
|
|
# Chain: this rsync -> TrueNAS /mnt/glacier/debyltechcloud (05:00 ZFS
|
|
# snapshot) -> the personal "iDrive E2 Backup" cloud-sync task, which pushes
|
|
# /mnt/glacier to the personal iDrive e2 bucket. Unlike /skudakcloud/**,
|
|
# /skudakapps/** and /skudakgit/**, there is NO exclude for /debyltechcloud/**
|
|
# on that task, and there must not be one -- that inclusion IS the offsite
|
|
# copy. If that ever changes, give it its own cloud-sync task first.
|
|
- include_tasks: containers/cloud-backup.yml
|
|
vars:
|
|
backup_name: debyltech-cloud
|
|
data_path: "{{ cloud_debyltech_path }}/data"
|
|
config_path: "{{ cloud_debyltech_path }}/config"
|
|
db_container: debyltech-cloud-db
|
|
ssh_key_path: /etc/ssh/backup_keys/debyltech-cloud
|
|
ssh_key_content: "{{ cloud_debyltech_backup_ssh_key }}"
|
|
ssh_user: debyltechcloud
|
|
remote_path: /mnt/glacier/debyltechcloud
|
|
script_path: /usr/local/bin/debyltech-cloud-backup.sh
|
|
# data/ is mode 770 here too; see skudak/cloud.yml.
|
|
backup_rsync_extra_args: "--chmod=Du=rwx,Dgo=rx"
|
|
# Between the 04:00 personal and 04:30 Skudak runs, before the 05:00
|
|
# TrueNAS snapshot.
|
|
backup_oncalendar: "*-*-* 04:15:00"
|