fix(debyltech-cloud): external signers, proxy trust, light-only mail
- Enable LibreSign's email identify method (click-to-sign, no account creation), remove the stamp background and collect signer metadata. On Skudak these were only ever set in the admin UI. Without the email method a fresh instance answers "No signers." for any outside address. The verify script now asserts it. - Store add_footer=true explicitly. The code already defaults to it, but the 14.2 admin page shows unset as unchecked. - trusted_proxies = the container's own address, read per deploy. Behind rootless port forwarding every request arrives from it, so without this X-Forwarded-For was ignored and every client shared one IP for brute-force throttling. - maintenance_window_start and default_phone_region, which clears the setup warnings. - Mail declares color-scheme "light only" so Apple Mail's dark mode doesn't repaint the white ground and bury the black wordmark (asserted in verify). - Idempotency: redis image fully qualified (docker.io/library/...), the debyltechmail copy owned by the mapped www-data uid, and theming compared before setting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fa5bbf8e54
commit
9ce9610965
@@ -188,6 +188,21 @@ class DebyltechEMailTemplate extends EMailTemplate {
|
|||||||
'font-size:26px;font-weight:300;letter-spacing:-0.02em',
|
'font-size:26px;font-weight:300;letter-spacing:-0.02em',
|
||||||
$this->heading,
|
$this->heading,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Opt out of mail-client dark mode. Without this Apple Mail repaints
|
||||||
|
// the white ground charcoal on its own, and the black wordmark all but
|
||||||
|
// disappears. Swapping to a white logo under prefers-color-scheme is
|
||||||
|
// NOT a fix: with Apple Mail's "Use dark backgrounds for messages" off,
|
||||||
|
// the query still matches while the ground stays white, leaving a
|
||||||
|
// white-on-white logo. This mail is designed light; render it light.
|
||||||
|
$this->head = str_replace(
|
||||||
|
'</head>',
|
||||||
|
'<meta name="color-scheme" content="light only">'
|
||||||
|
. '<meta name="supported-color-schemes" content="light only">'
|
||||||
|
. '<style type="text/css">:root{color-scheme:light only;supported-color-schemes:light only}</style>'
|
||||||
|
. '</head>',
|
||||||
|
$this->head,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -370,6 +370,58 @@
|
|||||||
register: debyltech_enum_fullmatch
|
register: debyltech_enum_fullmatch
|
||||||
changed_when: "'is now set to' in debyltech_enum_fullmatch.stdout"
|
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
|
- name: drop libresign validation QR code from signed-PDF footer in debyltech-cloud
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ podman_user }}"
|
become_user: "{{ podman_user }}"
|
||||||
@@ -444,6 +496,23 @@
|
|||||||
register: debyltechmail_enable
|
register: debyltechmail_enable
|
||||||
changed_when: "'already enabled' not in debyltechmail_enable.stdout"
|
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
|
# 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
|
# changes (Skudak's equivalents report changed on every run). Values are
|
||||||
# single-quoted into the shell, so the backslashes in mail_template_class pass
|
# single-quoted into the shell, so the backslashes in mail_template_class pass
|
||||||
@@ -467,6 +536,10 @@
|
|||||||
loop:
|
loop:
|
||||||
- {k: overwrite.cli.url, v: "https://{{ cloud_debyltech_server_name }}"}
|
- {k: overwrite.cli.url, v: "https://{{ cloud_debyltech_server_name }}"}
|
||||||
- {k: overwriteprotocol, v: https}
|
- {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}
|
||||||
- {k: loglevel, v: "2", t: integer}
|
- {k: loglevel, v: "2", t: integer}
|
||||||
- {k: log_rotate_size, v: "10485760", t: integer}
|
- {k: log_rotate_size, v: "10485760", t: integer}
|
||||||
- {k: mail_template_class, v: "OCA\\Debyltechmail\\Mail\\DebyltechEMailTemplate"}
|
- {k: mail_template_class, v: "OCA\\Debyltechmail\\Mail\\DebyltechEMailTemplate"}
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ if (!str_contains($text, 'official document-signing request')) {
|
|||||||
if (!str_contains($html, 'debyltech.com/legal/privacy') || !str_contains($html, 'debyltech.com/legal/tos')) {
|
if (!str_contains($html, 'debyltech.com/legal/privacy') || !str_contains($html, 'debyltech.com/legal/tos')) {
|
||||||
$failures[] = 'privacy/terms links missing from footer';
|
$failures[] = 'privacy/terms links missing from footer';
|
||||||
}
|
}
|
||||||
|
if (!str_contains($html, 'content="light only"')) {
|
||||||
|
$failures[] = 'color-scheme "light only" meta missing -- dark-mode mail clients will repaint '
|
||||||
|
. 'the ground and bury the black wordmark (did upstream rename </head> in $head?)';
|
||||||
|
}
|
||||||
if (preg_match('/[»«]/u', $html)) {
|
if (preg_match('/[»«]/u', $html)) {
|
||||||
$failures[] = 'German guillemets survived into the body';
|
$failures[] = 'German guillemets survived into the body';
|
||||||
}
|
}
|
||||||
@@ -131,6 +135,20 @@ if ($appConfig->getValueString('core', 'shareapi_restrict_user_enumeration_full_
|
|||||||
. 'unset or "yes"; it is NOT the knob for the account-owned-email problem.';
|
. 'unset or "yes"; it is NOT the knob for the account-owned-email problem.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Outside signers are invited by address; without an enabled email identify
|
||||||
|
// method the signer search returns "No signers." for any non-account email.
|
||||||
|
$methods = json_decode($appConfig->getValueString('libresign', 'identify_methods', '[]'), true) ?: [];
|
||||||
|
$emailOn = false;
|
||||||
|
foreach ($methods as $m) {
|
||||||
|
if (($m['name'] ?? '') === 'email' && !empty($m['enabled'])) {
|
||||||
|
$emailOn = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$emailOn) {
|
||||||
|
$failures[] = 'libresign email identify method is not enabled -- outside addresses '
|
||||||
|
. 'cannot be added as signers ("No signers.")';
|
||||||
|
}
|
||||||
|
|
||||||
$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