Add a scoped API user for the membership app

The user can read and manage subscribers on the eom.dev list only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-06 20:27:42 -05:00
co-authored by Claude Opus 5.5
parent 22a7ffe676
commit 3dd9cee930
4 changed files with 58 additions and 0 deletions
+9
View File
@@ -72,6 +72,15 @@ listmonk_lists:
description: >-
One short email per premiere, plus the monthly essay. Nothing else.
# API user for the www membership app (Authorization: token user:token).
# It can read and manage subscribers on listmonk_api_lists only. Empty token:
# no API user. listmonk caches API users at startup; the role restarts it.
listmonk_api_user: membership
listmonk_api_token: ""
listmonk_api_role: Membership sync
listmonk_api_lists:
- eom.dev
# Drop the sample lists, subscribers and campaign created by --install.
listmonk_remove_sample_data: true
+2
View File
@@ -11,3 +11,5 @@
vars:
listmonk_db_password: "{{ listmonk_postgresql_password }}"
listmonk_oidc_client_secret: "{{ listmonk_dex_client_secret }}"
# Same value as LISTMONK_API_TOKEN in the www "membership" Secret.
listmonk_api_token: "{{ listmonk_membership_api_token }}"
+46
View File
@@ -103,6 +103,52 @@ WITH upd AS (
SELECT count(*) FROM upd;
{% endfor %}
{% if listmonk_api_token | length > 0 %}
{#- API user for the membership app: subscribers on listmonk_api_lists only. #}
WITH ins AS (
INSERT INTO roles (type, name, permissions)
SELECT 'user', {{ q(listmonk_api_role) }}, ARRAY['subscribers:get', 'subscribers:manage']
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE type = 'user' AND name = {{ q(listmonk_api_role) }})
RETURNING id
)
SELECT count(*) FROM ins;
WITH ins AS (
INSERT INTO roles (type, name)
SELECT 'list', {{ q(listmonk_api_role) }}
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE type = 'list' AND name = {{ q(listmonk_api_role) }} AND parent_id IS NULL)
RETURNING id
)
SELECT count(*) FROM ins;
WITH parent AS (
SELECT id FROM roles WHERE type = 'list' AND name = {{ q(listmonk_api_role) }} AND parent_id IS NULL
), ins AS (
INSERT INTO roles (type, parent_id, list_id, permissions)
SELECT 'list', parent.id, l.id, ARRAY['list:get', 'list:manage']
FROM parent, lists l
WHERE l.name IN ({% for n in listmonk_api_lists %}{{ q(n) }}{{ ',' if not loop.last }}{% endfor %})
ON CONFLICT (parent_id, list_id) DO NOTHING
RETURNING id
)
SELECT count(*) FROM ins;
WITH ur AS (
SELECT id FROM roles WHERE type = 'user' AND name = {{ q(listmonk_api_role) }}
), lr AS (
SELECT id FROM roles WHERE type = 'list' AND name = {{ q(listmonk_api_role) }} AND parent_id IS NULL
), up AS (
INSERT INTO users (username, password_login, password, email, name, type, user_role_id, list_role_id, status)
SELECT {{ q(listmonk_api_user) }}, false, {{ q(listmonk_api_token | hash('sha256')) }},
{{ q(listmonk_api_user ~ '@api') }}, {{ q(listmonk_api_user) }}, 'api', ur.id, lr.id, 'enabled'
FROM ur, lr
ON CONFLICT (username) DO UPDATE
SET password = EXCLUDED.password, user_role_id = EXCLUDED.user_role_id,
list_role_id = EXCLUDED.list_role_id, status = 'enabled', updated_at = NOW()
WHERE (users.password, users.user_role_id, users.list_role_id, users.status)
IS DISTINCT FROM (EXCLUDED.password, EXCLUDED.user_role_id, EXCLUDED.list_role_id, 'enabled'::user_status)
RETURNING id
)
SELECT count(*) FROM up;
{% endif %}
{% if listmonk_remove_sample_data | bool %}
WITH del AS (
DELETE FROM campaigns
+1
View File
@@ -17,6 +17,7 @@
listmonk_admin_password: render-only
listmonk_smtp_password: render-only
listmonk_oidc_client_secret: render-only
listmonk_api_token: render-only-api-token
- name: Output directory
ansible.builtin.file:
path: "{{ out }}"