Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4a3d13870
|
@@ -39,6 +39,15 @@ The eom.dev playbook maps them from the Vault names `listmonk_postgresql_passwor
|
|||||||
1. In LDAP, create the posix account `news` with `mail: news@eom.dev` and add it to `cn=Postfix Users,ou=Postfix,ou=Services,dc=eom,dc=dev`. Its password is `listmonk_smtp_password`.
|
1. In LDAP, create the posix account `news` with `mail: news@eom.dev` and add it to `cn=Postfix Users,ou=Postfix,ou=Services,dc=eom,dc=dev`. Its password is `listmonk_smtp_password`.
|
||||||
2. Add the `listmonk` client to Dex (`ericomeehan.dex/playbooks/deploy-eom.yml`) and run that play.
|
2. Add the `listmonk` client to Dex (`ericomeehan.dex/playbooks/deploy-eom.yml`) and run that play.
|
||||||
|
|
||||||
|
## API users
|
||||||
|
|
||||||
|
| User | Can do | Token |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `membership` | Read and manage subscribers on `eom.dev` (signup opt-in, profile toggle) | `listmonk_api_token` (Vault); same value as `LISTMONK_API_TOKEN` in the www `membership` Secret |
|
||||||
|
| `newsletter` | Create, edit, test-send and schedule campaigns to `eom.dev`; no subscriber data or settings | Generated by the role, stored in `Secret/listmonk-newsletter-api` (`url`, `username`, `token`) |
|
||||||
|
|
||||||
|
Neither user can read settings or other lists. To rotate the newsletter token, delete its Secret and run the play again.
|
||||||
|
|
||||||
## Embedding a signup form
|
## Embedding a signup form
|
||||||
|
|
||||||
listmonk accepts a plain HTML POST, so any eom.dev page can include a form:
|
listmonk accepts a plain HTML POST, so any eom.dev page can include a form:
|
||||||
|
|||||||
@@ -81,6 +81,16 @@ listmonk_api_role: Membership sync
|
|||||||
listmonk_api_lists:
|
listmonk_api_lists:
|
||||||
- eom.dev
|
- eom.dev
|
||||||
|
|
||||||
|
# API user for the scheduled newsletter task: create, edit and schedule
|
||||||
|
# campaigns to listmonk_newsletter_api_lists; no subscriber data or settings.
|
||||||
|
# Its token is generated once and kept in Secret <name>-newsletter-api
|
||||||
|
# (keys: url, username, token) for the task to read with kubectl.
|
||||||
|
listmonk_newsletter_api_enabled: true
|
||||||
|
listmonk_newsletter_api_user: newsletter
|
||||||
|
listmonk_newsletter_api_role: Newsletter automation
|
||||||
|
listmonk_newsletter_api_lists:
|
||||||
|
- eom.dev
|
||||||
|
|
||||||
# Drop the sample lists, subscribers and campaign created by --install.
|
# Drop the sample lists, subscribers and campaign created by --install.
|
||||||
listmonk_remove_sample_data: true
|
listmonk_remove_sample_data: true
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,46 @@
|
|||||||
wait: true
|
wait: true
|
||||||
wait_timeout: "{{ listmonk_wait_timeout }}"
|
wait_timeout: "{{ listmonk_wait_timeout }}"
|
||||||
|
|
||||||
|
- name: Read the newsletter automation credentials
|
||||||
|
kubernetes.core.k8s_info:
|
||||||
|
kubeconfig: "{{ listmonk_kubeconfig | default(omit, true) }}"
|
||||||
|
context: "{{ listmonk_kube_context | default(omit, true) }}"
|
||||||
|
kind: Secret
|
||||||
|
namespace: "{{ listmonk_namespace }}"
|
||||||
|
name: "{{ listmonk_name }}-newsletter-api"
|
||||||
|
register: _listmonk_newsletter_secret
|
||||||
|
no_log: true
|
||||||
|
when: listmonk_newsletter_api_enabled | bool
|
||||||
|
|
||||||
|
# Generated once, then reused; delete the Secret to rotate the token.
|
||||||
|
- name: Use or generate the newsletter automation token
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
listmonk_newsletter_api_token: >-
|
||||||
|
{{ (_listmonk_newsletter_secret.resources[0].data.token | b64decode)
|
||||||
|
if _listmonk_newsletter_secret.resources | length > 0
|
||||||
|
else lookup('ansible.builtin.password', '/dev/null', chars=['ascii_letters', 'digits'], length=48) }}
|
||||||
|
no_log: true
|
||||||
|
when: listmonk_newsletter_api_enabled | bool
|
||||||
|
|
||||||
|
- name: Store the newsletter automation credentials
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
kubeconfig: "{{ listmonk_kubeconfig | default(omit, true) }}"
|
||||||
|
context: "{{ listmonk_kube_context | default(omit, true) }}"
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: "{{ listmonk_name }}-newsletter-api"
|
||||||
|
namespace: "{{ listmonk_namespace }}"
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
url: "{{ listmonk_root_url }}"
|
||||||
|
username: "{{ listmonk_newsletter_api_user }}"
|
||||||
|
token: "{{ listmonk_newsletter_api_token }}"
|
||||||
|
no_log: true
|
||||||
|
when: listmonk_newsletter_api_enabled | bool
|
||||||
|
|
||||||
# Settings, Dex admins and lists live in listmonk's database, not its config.
|
# Settings, Dex admins and lists live in listmonk's database, not its config.
|
||||||
- name: Converge database-held configuration
|
- name: Converge database-held configuration
|
||||||
kubernetes.core.k8s_exec:
|
kubernetes.core.k8s_exec:
|
||||||
|
|||||||
+31
-13
@@ -103,41 +103,59 @@ WITH upd AS (
|
|||||||
SELECT count(*) FROM upd;
|
SELECT count(*) FROM upd;
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if listmonk_api_token | length > 0 %}
|
{#- Scoped API users: the membership app (subscribers) and the newsletter
|
||||||
{#- API user for the membership app: subscribers on listmonk_api_lists only. #}
|
automation (campaigns). Each gets a user role and a list role limited to
|
||||||
|
its lists. listmonk caches API users at startup; the play restarts it. #}
|
||||||
|
{%- set api_users = [] %}
|
||||||
|
{%- if listmonk_api_token | length > 0 %}
|
||||||
|
{%- set _ = api_users.append({'username': listmonk_api_user, 'token': listmonk_api_token, 'role': listmonk_api_role,
|
||||||
|
'permissions': ['subscribers:get', 'subscribers:manage'], 'lists': listmonk_api_lists}) %}
|
||||||
|
{%- endif %}
|
||||||
|
{%- if listmonk_newsletter_api_token | default('') | length > 0 %}
|
||||||
|
{%- set _ = api_users.append({'username': listmonk_newsletter_api_user, 'token': listmonk_newsletter_api_token, 'role': listmonk_newsletter_api_role,
|
||||||
|
'permissions': ['campaigns:get', 'campaigns:manage', 'campaigns:send', 'templates:get'], 'lists': listmonk_newsletter_api_lists}) %}
|
||||||
|
{%- endif %}
|
||||||
|
{% for api in api_users %}
|
||||||
WITH ins AS (
|
WITH ins AS (
|
||||||
INSERT INTO roles (type, name, permissions)
|
INSERT INTO roles (type, name, permissions)
|
||||||
SELECT 'user', {{ q(listmonk_api_role) }}, ARRAY['subscribers:get', 'subscribers:manage']
|
SELECT 'user', {{ q(api.role) }}, ARRAY[{% for p in api.permissions %}{{ q(p) }}{{ ',' if not loop.last }}{% endfor %}]
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE type = 'user' AND name = {{ q(listmonk_api_role) }})
|
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE type = 'user' AND name = {{ q(api.role) }})
|
||||||
RETURNING id
|
RETURNING id
|
||||||
)
|
)
|
||||||
SELECT count(*) FROM ins;
|
SELECT count(*) FROM ins;
|
||||||
|
WITH upd AS (
|
||||||
|
UPDATE roles SET permissions = ARRAY[{% for p in api.permissions %}{{ q(p) }}{{ ',' if not loop.last }}{% endfor %}], updated_at = NOW()
|
||||||
|
WHERE type = 'user' AND name = {{ q(api.role) }}
|
||||||
|
AND permissions IS DISTINCT FROM ARRAY[{% for p in api.permissions %}{{ q(p) }}{{ ',' if not loop.last }}{% endfor %}]
|
||||||
|
RETURNING id
|
||||||
|
)
|
||||||
|
SELECT count(*) FROM upd;
|
||||||
WITH ins AS (
|
WITH ins AS (
|
||||||
INSERT INTO roles (type, name)
|
INSERT INTO roles (type, name)
|
||||||
SELECT 'list', {{ q(listmonk_api_role) }}
|
SELECT 'list', {{ q(api.role) }}
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE type = 'list' AND name = {{ q(listmonk_api_role) }} AND parent_id IS NULL)
|
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE type = 'list' AND name = {{ q(api.role) }} AND parent_id IS NULL)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
)
|
)
|
||||||
SELECT count(*) FROM ins;
|
SELECT count(*) FROM ins;
|
||||||
WITH parent AS (
|
WITH parent AS (
|
||||||
SELECT id FROM roles WHERE type = 'list' AND name = {{ q(listmonk_api_role) }} AND parent_id IS NULL
|
SELECT id FROM roles WHERE type = 'list' AND name = {{ q(api.role) }} AND parent_id IS NULL
|
||||||
), ins AS (
|
), ins AS (
|
||||||
INSERT INTO roles (type, parent_id, list_id, permissions)
|
INSERT INTO roles (type, parent_id, list_id, permissions)
|
||||||
SELECT 'list', parent.id, l.id, ARRAY['list:get', 'list:manage']
|
SELECT 'list', parent.id, l.id, ARRAY['list:get', 'list:manage']
|
||||||
FROM parent, lists l
|
FROM parent, lists l
|
||||||
WHERE l.name IN ({% for n in listmonk_api_lists %}{{ q(n) }}{{ ',' if not loop.last }}{% endfor %})
|
WHERE l.name IN ({% for n in api.lists %}{{ q(n) }}{{ ',' if not loop.last }}{% endfor %})
|
||||||
ON CONFLICT (parent_id, list_id) DO NOTHING
|
ON CONFLICT (parent_id, list_id) DO NOTHING
|
||||||
RETURNING id
|
RETURNING id
|
||||||
)
|
)
|
||||||
SELECT count(*) FROM ins;
|
SELECT count(*) FROM ins;
|
||||||
WITH ur AS (
|
WITH ur AS (
|
||||||
SELECT id FROM roles WHERE type = 'user' AND name = {{ q(listmonk_api_role) }}
|
SELECT id FROM roles WHERE type = 'user' AND name = {{ q(api.role) }}
|
||||||
), lr AS (
|
), lr AS (
|
||||||
SELECT id FROM roles WHERE type = 'list' AND name = {{ q(listmonk_api_role) }} AND parent_id IS NULL
|
SELECT id FROM roles WHERE type = 'list' AND name = {{ q(api.role) }} AND parent_id IS NULL
|
||||||
), up AS (
|
), up AS (
|
||||||
INSERT INTO users (username, password_login, password, email, name, type, user_role_id, list_role_id, status)
|
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')) }},
|
SELECT {{ q(api.username) }}, false, {{ q(api.token | hash('sha256')) }},
|
||||||
{{ q(listmonk_api_user ~ '@api') }}, {{ q(listmonk_api_user) }}, 'api', ur.id, lr.id, 'enabled'
|
{{ q(api.username ~ '@api') }}, {{ q(api.username) }}, 'api', ur.id, lr.id, 'enabled'
|
||||||
FROM ur, lr
|
FROM ur, lr
|
||||||
ON CONFLICT (username) DO UPDATE
|
ON CONFLICT (username) DO UPDATE
|
||||||
SET password = EXCLUDED.password, user_role_id = EXCLUDED.user_role_id,
|
SET password = EXCLUDED.password, user_role_id = EXCLUDED.user_role_id,
|
||||||
@@ -147,7 +165,7 @@ WITH ur AS (
|
|||||||
RETURNING id
|
RETURNING id
|
||||||
)
|
)
|
||||||
SELECT count(*) FROM up;
|
SELECT count(*) FROM up;
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if listmonk_remove_sample_data | bool %}
|
{% if listmonk_remove_sample_data | bool %}
|
||||||
WITH del AS (
|
WITH del AS (
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
listmonk_smtp_password: render-only
|
listmonk_smtp_password: render-only
|
||||||
listmonk_oidc_client_secret: render-only
|
listmonk_oidc_client_secret: render-only
|
||||||
listmonk_api_token: render-only-api-token
|
listmonk_api_token: render-only-api-token
|
||||||
|
listmonk_newsletter_api_token: render-only-newsletter-token
|
||||||
- name: Output directory
|
- name: Output directory
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ out }}"
|
path: "{{ out }}"
|
||||||
|
|||||||
Reference in New Issue
Block a user