feat(debyltech-cloud): lock customers to read-only, no discovery
Customers are admin-created accounts in per-customer groups. They should
read what staff share with them and nothing else. Checked against a test
customer in the UI, and by running the sharee and contacts-menu search
services as that user.
- shareapi_exclude_groups=allow, list [admin]: only staff can share. Exclude
mode ("yes") only disables sharing for users whose groups are ALL
excluded, so it never catches a customer in their own group.
- User/group autocomplete off. By default a customer typing "bas" found the
owner's account. Staff share by exact group name; LibreSign signers are
found by email.
- New shares default to View only (shareapi_default_permissions=1).
- files default_quota 0 B, so customers get no personal storage. Staff in
cloud_debyltech_staff_users are exempted (skipped if not yet created).
- No "Leon Green" sample contact in new address books.
- The verify script fails the deploy if any isolation setting drifts.
- README: staff and customer onboarding checklist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
be50798096
commit
c4fe506860
@@ -154,6 +154,38 @@ LibreSign setup failures and buys nothing at this scale.
|
|||||||
(LibreSign issue #4872).
|
(LibreSign issue #4872).
|
||||||
|
|
||||||
|
|
||||||
|
## cloud.debyltech.com accounts
|
||||||
|
|
||||||
|
Registration is off, so every account is created by an admin in
|
||||||
|
Settings → Accounts. The isolation policy in
|
||||||
|
`tasks/containers/debyltech/cloud.yml` is enforced on every deploy and checked
|
||||||
|
by the verify script.
|
||||||
|
|
||||||
|
**Staff**: add to the `admin` group (the only group allowed to share) and to
|
||||||
|
`cloud_debyltech_staff_users` in `defaults/main.yml`, so the next deploy
|
||||||
|
exempts them from the 0 B default quota. Until then, set the account's quota
|
||||||
|
to *Unlimited* by hand.
|
||||||
|
|
||||||
|
**Customer**:
|
||||||
|
1. Create a group per customer, e.g. `Customer - Acme`. Use a consistent
|
||||||
|
prefix: autocomplete is off, so you share by typing the **exact** group
|
||||||
|
name.
|
||||||
|
2. Create the account with only their email filled in and no password, in
|
||||||
|
that group. Nextcloud sends a branded welcome email with a set-password
|
||||||
|
link.
|
||||||
|
3. Share a folder (for example `Customers/Acme`) with the group. It defaults
|
||||||
|
to **View only**; choose *Allow editing* only if they should upload.
|
||||||
|
|
||||||
|
What a customer gets:
|
||||||
|
- read-only access to what's shared with them
|
||||||
|
- no personal storage (0 B quota)
|
||||||
|
- no sharing or public links
|
||||||
|
- no view of other accounts or groups: the share search and contacts menu
|
||||||
|
return nothing
|
||||||
|
|
||||||
|
Signature requests to customers need no account: use LibreSign with their
|
||||||
|
email address.
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
Every container runs with `log_driver=journald`, so container stdout lands in
|
Every container runs with `log_driver=journald`, so container stdout lands in
|
||||||
|
|||||||
@@ -241,6 +241,14 @@ libresign_debyltech_cert_l: Newbury
|
|||||||
# cloud_debyltech_smtp_user / cloud_debyltech_smtp_pass in the vault.
|
# cloud_debyltech_smtp_user / cloud_debyltech_smtp_pass in the vault.
|
||||||
cloud_debyltech_smtp_host: email-smtp.us-east-1.amazonaws.com
|
cloud_debyltech_smtp_host: email-smtp.us-east-1.amazonaws.com
|
||||||
cloud_debyltech_smtp_port: 465
|
cloud_debyltech_smtp_port: 465
|
||||||
|
# Staff vs customers (see "customer isolation" in containers/debyltech/cloud.yml).
|
||||||
|
# Only this group may share; everyone else -- customers -- can only read what
|
||||||
|
# is shared with them. Staff accounts are exempted from the 0 B default quota.
|
||||||
|
# Accounts listed here that do not exist yet are skipped, not created.
|
||||||
|
cloud_debyltech_staff_group: admin
|
||||||
|
cloud_debyltech_staff_users:
|
||||||
|
- admin
|
||||||
|
- bastian@debyltech.com
|
||||||
|
|
||||||
|
|
||||||
# Legacy nginx/ModSecurity configuration removed - Caddy provides built-in security
|
# Legacy nginx/ModSecurity configuration removed - Caddy provides built-in security
|
||||||
|
|||||||
@@ -468,6 +468,68 @@
|
|||||||
insertbefore: '^\);'
|
insertbefore: '^\);'
|
||||||
create: false
|
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 }}"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Branding: debyltechmail, cloned from skudakmail. custom_apps is a persisted
|
# Branding: debyltechmail, cloned from skudakmail. custom_apps is a persisted
|
||||||
# bind mount, so only enabling and config need reasserting.
|
# bind mount, so only enabling and config need reasserting.
|
||||||
|
|||||||
@@ -149,6 +149,22 @@ if (!$emailOn) {
|
|||||||
. 'cannot be added as signers ("No signers.")';
|
. 'cannot be added as signers ("No signers.")';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Customer isolation (see the policy task in containers/debyltech/cloud.yml).
|
||||||
|
// A stray click in Settings > Sharing can undo any of these, and each one
|
||||||
|
// quietly re-exposes customers to one another or lets them share onward.
|
||||||
|
$isolation = [
|
||||||
|
'shareapi_exclude_groups' => 'allow',
|
||||||
|
'shareapi_exclude_groups_list' => json_encode(['{{ cloud_debyltech_staff_group }}']),
|
||||||
|
'shareapi_allow_share_dialog_user_enumeration' => 'no',
|
||||||
|
'shareapi_default_permissions' => '1',
|
||||||
|
];
|
||||||
|
foreach ($isolation as $key => $want) {
|
||||||
|
$have = \OC::$server->get(\OCP\IConfig::class)->getAppValue('core', $key, '<unset>');
|
||||||
|
if ($have !== $want) {
|
||||||
|
$failures[] = "core $key is \"$have\" -- expected \"$want\" (customer isolation)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$identDocs = $appConfig->getValueString('libresign', 'identification_documents', '');
|
$identDocs = $appConfig->getValueString('libresign', 'identification_documents', '');
|
||||||
if ($identDocs !== '0') {
|
if ($identDocs !== '0') {
|
||||||
$failures[] = 'libresign identification_documents is "' . $identDocs
|
$failures[] = 'libresign identification_documents is "' . $identDocs
|
||||||
|
|||||||
Reference in New Issue
Block a user