Two guards, both from real incidents in the same session. 1. invoice-create.sh — chronology (CGI art. 289). Dolibarr assigns the number at validation, in creation order, so issuing a document dated BEFORE the last one already issued gives a higher number to an earlier date. The July plan walked straight into it: the M3 deferred part is due 2026-10-23 and must be issued at D-60 (24/08) to stay under the L.441-10 I ceiling, while the M4 fixed part is dated 23/08 — issue them in the wrong order and the numbering breaks. The guard reads the last issued document of the same kind and refuses an earlier date, with ARCO_ALLOW_BACKDATE as a loud, documented override. Verified: refuses a 01/07 invoice against FAC008 (23/07), accepts 23/08. 2. test/scripts/guard.ts — production opt-in. The sandbox-only guard had no way to express a deliberate production run, so any prod work meant bypassing it entirely (which is how guards die). Production now requires BOTH ARCO_ALLOW_PRODUCTION=<exact host> and ARCO_PROD_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD, and prints a banner. Nothing reaches prod by inheriting an ambient variable. Also fixes a misleading "(sandbox verified)" log that printed even on prod. grantAgentRight.ts joins the repo (it was never committed) and gains --revoke, so a temporarily elevated right can be handed back — used today to attach a payment in production and revoked immediately after. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
90 lines
3.3 KiB
TypeScript
90 lines
3.3 KiB
TypeScript
/*
|
|
Grant a Dolibarr permission to the sandbox write agent, through the admin UI.
|
|
|
|
The REST API cannot do this (the write agent is non-admin by design), and the
|
|
permission set it ships with is deliberately narrow: invoices, thirdparties,
|
|
payments. Anything beyond that is an explicit, auditable grant — which is what
|
|
this script performs, one right at a time, on the sandbox only.
|
|
|
|
Run:
|
|
DOLIBARR_ADDRESS=https://erp-sandbox.arcodange.lab \
|
|
deno run -A test/grantAgentRight.ts --user-id 4 --match "proposition"
|
|
*/
|
|
import "load_dotenv";
|
|
import { chromium } from "playwright";
|
|
import login from "./scripts/login.ts";
|
|
import { assertSandbox } from "./scripts/guard.ts";
|
|
|
|
const argv = Deno.args;
|
|
const pick = (f: string, d: string) =>
|
|
argv.includes(f) ? argv[argv.indexOf(f) + 1] : d;
|
|
const userId = pick("--user-id", "4");
|
|
const match = pick("--match", "");
|
|
const dryRun = argv.includes("--dry-run");
|
|
const revoke = argv.includes("--revoke");
|
|
const shotDir = Deno.env.get("SHOT_DIR") ?? "/tmp";
|
|
|
|
if (!match) {
|
|
console.error("--match <substring of the permission label> is required");
|
|
Deno.exit(2);
|
|
}
|
|
|
|
const dolibarrAddress = assertSandbox();
|
|
console.log(`target: ${dolibarrAddress} (guard passed)`);
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
const context = await browser.newContext({ locale: "fr-FR" });
|
|
const page = await context.newPage();
|
|
|
|
try {
|
|
await login.doAdminLogin({
|
|
page,
|
|
dolibarrAddress,
|
|
adminCredentials: {
|
|
username: Deno.env.get("DOLI_ADMIN_LOGIN") || "undefined",
|
|
password: Deno.env.get("DOLI_ADMIN_PASSWORD") || "undefined",
|
|
},
|
|
});
|
|
|
|
await page.goto(`${dolibarrAddress}/user/perms.php?id=${userId}`);
|
|
const who = await page.locator("h1, .titre, .nowrap").first()
|
|
.textContent({ timeout: 5000 }).catch(() => "(title not found)");
|
|
console.log(`permissions page for: ${who?.trim()}`);
|
|
console.log(`url: ${page.url()}`);
|
|
|
|
// Rows whose label matches, that are not already granted (a granted row shows
|
|
// the "remove" link instead of the "add" one).
|
|
const rows = page.locator("tr", {
|
|
has: page.locator("td", { hasText: new RegExp(match, "i") }),
|
|
});
|
|
const total = await rows.count();
|
|
console.log(`rows matching /${match}/i: ${total}`);
|
|
|
|
let granted = 0;
|
|
for (let i = 0; i < total; i++) {
|
|
const row = rows.nth(i);
|
|
const label = (await row.textContent())?.replace(/\s+/g, " ").trim().slice(0, 90);
|
|
const action = revoke ? "delrights" : "addrights";
|
|
const addLink = row.locator(`a[href*="action=${action}"]`);
|
|
if (await addLink.count() === 0) {
|
|
console.log(` [skip, already ${revoke ? "revoked" : "granted"} or not actionable] ${label}`);
|
|
continue;
|
|
}
|
|
if (dryRun) {
|
|
console.log(` [dry-run would ${revoke ? "revoke" : "grant"}] ${label}`);
|
|
continue;
|
|
}
|
|
await addLink.first().click();
|
|
await page.waitForLoadState("networkidle");
|
|
granted++;
|
|
console.log(` [${revoke ? "revoked" : "granted"}] ${label}`);
|
|
// The DOM is rebuilt after each grant — re-resolve on the next iteration.
|
|
await page.goto(`${dolibarrAddress}/user/perms.php?id=${userId}`);
|
|
}
|
|
console.log(`${revoke ? "revoked" : "granted"} ${granted} right(s)`);
|
|
await page.screenshot({ path: `${shotDir}/perms.png`, fullPage: true });
|
|
} finally {
|
|
await context.close();
|
|
await browser.close();
|
|
}
|