Files

84 lines
4.0 KiB
JavaScript

/**
* Thin authenticated REST client for the destination/template/user endpoints.
* Used by the DL suite to verify what the UI actually persisted — the form can
* echo your input while the stored value differs (trimming, lowercasing,
* empty entries), so every storage assertion reads the API, not the field.
*/
const BASE = process.env.ZO_BASE_URL || 'http://localhost:5080';
const ORG = process.env.ORGNAME || 'default';
const USER = process.env.ZO_ROOT_USER_EMAIL || 'root@example.com';
// The repo's documented default for local runs — the same value playwright.yml
// commits publicly. Not a secret; CI and any real environment override it.
const PASS = process.env.ZO_ROOT_USER_PASSWORD || 'Complexpass#123';
const AUTH = 'Basic ' + Buffer.from(`${USER}:${PASS}`).toString('base64');
async function req(method, path, body) {
const res = await fetch(`${BASE}/api/${ORG}${path}`, {
method,
headers: { Authorization: AUTH, 'Content-Type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await res.text();
let json = null;
try { json = JSON.parse(text); } catch (_) { /* non-JSON body */ }
return { status: res.status, body: json, text };
}
const getDestination = (name) => req('GET', `/alerts/destinations/${name}`);
const listDestinations = () => req('GET', '/alerts/destinations');
const createDestination = (payload) => req('POST', '/alerts/destinations', payload);
const deleteDestination = (name) => req('DELETE', `/alerts/destinations/${name}`);
const testDestination = (payload) => req('POST', '/alerts/destinations/test', payload);
const listTemplates = () => req('GET', '/alerts/templates');
/**
* A destination recipient must be a user of the org — see the membership gate in
* alerts/destinations.rs. A distribution-list alias is not a person, so the DL
* fan-out case has to enrol the alias before it can be addressed at all.
*
* role MUST be 'admin': UserRoleRequest::from() (common/src/meta/user.rs) matches
* the requested role string against get_roles() and, on no match, treats it as a
* CUSTOM role — which core/src/users.rs::post_user rejects outright with 400 when
* OpenFGA/RBAC is off (the OSS CI topology this suite actually runs in restricts
* the standard-role set to Admin only). A lower role 400s the create outright,
* which is worse for the "stray privileged account" concern this once tried to
* avoid — cleanup being flag-gated (see dlUserCreated below) is the real mitigation.
*/
const createOrgUser = (email) => req('POST', '/users', {
email, first_name: 'dl', last_name: 'alias',
password: PASS,
role: 'admin',
});
const deleteOrgUser = (email) => req('DELETE', `/users/${encodeURIComponent(email)}`);
const getTemplate = (name) => req('GET', `/alerts/templates/${name}`);
const listOrgUsers = () => req('GET', '/users');
const listServiceAccounts = () => req('GET', '/service_accounts');
/**
* Every address the recipients picker may offer — the org's users plus its
* service accounts, lowercased. It is the picker's own option source, so a case
* can prove an offered value really is an account of this org (and that an
* outsider is not one) instead of taking the list on trust.
*/
async function orgAccounts() {
const responses = await Promise.all([listOrgUsers(), listServiceAccounts()]);
const emails = responses.flatMap((r) =>
(Array.isArray(r.body?.data) ? r.body.data : [])
.map((account) => String(account?.email ?? '').toLowerCase()),
);
return [...new Set(emails.filter(Boolean))];
}
/** Stored recipients for a destination, or null when it does not exist. */
async function storedRecipients(name) {
const r = await getDestination(name);
if (r.status !== 200 || !r.body) return null;
return r.body.emails;
}
module.exports = {
BASE, ORG, req, getDestination, listDestinations, createDestination,
deleteDestination, testDestination, listTemplates, getTemplate, storedRecipients,
createOrgUser, deleteOrgUser, listOrgUsers, listServiceAccounts, orgAccounts,
};