diff --git a/ansible/roles/obsidian_headless/README.md b/ansible/roles/obsidian_headless/README.md index 4aaabd1..68d255e 100644 --- a/ansible/roles/obsidian_headless/README.md +++ b/ansible/roles/obsidian_headless/README.md @@ -25,7 +25,7 @@ only. Entry point: `playbooks/provision-wong-host.yml`. | `/srv/obsidian/` | `0710 obsync` | `user:multica:--x` | | `/srv/obsidian/vault/` | `0710 obsync` | `user:multica:--x`, so `multica` can traverse but not list | | `vault/everpure/`, `vault/wip/` | `0700 obsync` | none | - | `vault/inbox/` | `0770 obsync` | `user:multica:rwx`, plus the same as a **default** entry | + | `vault/inbox/` | `0770 obsync` | `user:multica:rwx` and `user:obsync:rwx`, each also as a **default** entry | On a directory with an ACL, the mode's group bits are the ACL mask. A `chmod 0700` would therefore void every named entry, which is why the @@ -38,6 +38,9 @@ only. Entry point: `playbooks/provision-wong-host.yml`. - `multica` writes, reads and deletes in `inbox/`. - A folder and file created by `obsync` under umask `0077` (the way Sync creates them) stay readable and writable by `multica`. + - What `multica` creates stays readable and writable by `obsync`. Those + files are owned `multica:multica`, so without the named `user:obsync` + default entry Sync could not read them and would never upload them. - `multica` is refused both listing the vault root and listing or writing `everpure/` and `wip/`. diff --git a/ansible/roles/obsidian_headless/tasks/layout.yml b/ansible/roles/obsidian_headless/tasks/layout.yml index f61ef7c..0303993 100644 --- a/ansible/roles/obsidian_headless/tasks/layout.yml +++ b/ansible/roles/obsidian_headless/tasks/layout.yml @@ -4,8 +4,13 @@ # / obsync 0710 + user::--x traverse, no listing # / obsync 0710 + user::--x traverse, no listing # // obsync 0700 nobody else -# // obsync 0770 + user::rwx, and the same as a -# DEFAULT entry, so new children inherit +# // obsync 0770 + user::rwx and user:obsync:rwx, +# each also as a DEFAULT entry, so new +# children inherit both +# +# The user:obsync entry matters for content the SHARED user creates: that is +# owned :, so without a named entry obsync falls into "other" (---) +# and Sync can neither read nor upload it. # # The owning group is obsync's private group, so its bits grant nobody. On a # directory that carries an ACL, the group bits of the mode ARE the ACL mask, @@ -62,6 +67,18 @@ loop_control: label: "{{ item.path }}" +# Each shared folder gets its user AND the service user, both as access and +# default entries. +- name: Layout | Build the shared-folder grants + ansible.builtin.set_fact: + _obh_grants: >- + {{ (obsidian_headless_shared_folders + + (obsidian_headless_shared_folders + | map(attribute='path') + | map('community.general.dict_kv', 'path') | list + | map('combine', {'user': obsidian_headless_user}) | list)) + | unique | list }} + - name: Layout | Grant shared folders (access and default entries) ansible.posix.acl: path: "{{ obsidian_headless_vault_path }}/{{ item.0.path }}" @@ -70,6 +87,6 @@ permissions: rwx default: "{{ item.1 }}" state: present - loop: "{{ obsidian_headless_shared_folders | product([false, true]) | list }}" + loop: "{{ _obh_grants | product([false, true]) | list }}" loop_control: label: "{{ item.0.path }} -> {{ item.0.user }}{{ ' (default)' if item.1 else '' }}" diff --git a/ansible/roles/obsidian_headless/tasks/verify.yml b/ansible/roles/obsidian_headless/tasks/verify.yml index aacaf72..9b54f70 100644 --- a/ansible/roles/obsidian_headless/tasks/verify.yml +++ b/ansible/roles/obsidian_headless/tasks/verify.yml @@ -18,6 +18,61 @@ changed_when: false check_mode: false +# The reverse direction: what the shared user writes, Sync must be able to +# read (to upload it) and rewrite (to merge remote edits into it). +- name: Verify | Content a shared user creates stays usable by the service (create) + ansible.builtin.command: + argv: + - /bin/sh + - -c + - 'd="$1/.obh-verify-rev"; mkdir -p "$d" && echo ok > "$d/note.md"' + - sh + - "{{ obsidian_headless_vault_path }}/{{ item.path }}" + become: true + become_user: "{{ item.user }}" + loop: "{{ obsidian_headless_shared_folders }}" + loop_control: + label: "{{ item.user }} -> {{ item.path }}" + changed_when: false + check_mode: false + +- name: Verify | Content a shared user creates stays usable by the service (use) + ansible.builtin.command: + argv: + - /bin/sh + - -c + - 'd="$1/.obh-verify-rev"; grep -q ok "$d/note.md" && echo more >> "$d/note.md" && echo ok > "$d/reply.md"' + - sh + - "{{ obsidian_headless_vault_path }}/{{ item.path }}" + become: true + become_user: "{{ obsidian_headless_user }}" + loop: "{{ obsidian_headless_shared_folders }}" + loop_control: + label: "{{ item.path }}" + register: _obh_reverse + changed_when: false + failed_when: false + check_mode: false + +- name: Verify | Remove the reverse probes + ansible.builtin.file: + path: "{{ obsidian_headless_vault_path }}/{{ item.path }}/.obh-verify-rev" + state: absent + loop: "{{ obsidian_headless_shared_folders }}" + loop_control: + label: "{{ item.path }}" + changed_when: false + +- name: Verify | The service can read and rewrite shared users' content + ansible.builtin.assert: + that: + - _obh_reverse.results | rejectattr('rc', 'equalto', 0) | list | length == 0 + fail_msg: >- + {{ obsidian_headless_user }} cannot read or write content a shared user + created; Sync would never upload it. Check the user:{{ obsidian_headless_user }} + default ACL entry on the shared folder. + quiet: true + # Simulates the service: obsync, umask 0077, creating a folder and a file the # way Sync does. The shared user must still reach both, via the default ACL. - name: Verify | Content the service creates in a shared folder inherits the grant (create)