fix(security): production read agent is now actually read-only
Migration applied to production, in the only order that does not break the promote flow: 1. Created `ai_agent_prod_prod_write` (id=5) with the narrow `prod-write` scope — invoices, payments, and document submission (needed by builddoc to regenerate a modified invoice's PDF). Verified functionally: reads pass, DELETE on an invoice returns 403. 2. Repointed the promote pipeline at that user's key. It no longer borrows the read skills' credential; if the key is absent it dies with the provisioning command rather than silently falling back. 3. Revoked 12 write/delete rights from `ai_agent` (id=3), the credential every read skill holds: create/modify on customer AND supplier invoices, thirdparties, contacts, thirdparty payment details, proposals, exports, accounting links — and delete on proposals, events, and GED documents. Verified after: invoices, thirdparties, contacts, products, proposals, supplier invoices and bank accounts all still read; creating an invoice returns `403 Forbidden: Insuffisant rights`. The documented posture and the real one finally agree. scopes.ts corrected against the live instance: 262 is NOT "créer/modifier les produits" as the first catalogue guessed but the `voir_tous` ACL extension — a READ right the skills depend on (without it, list endpoints return empty arrays instead of 403). Revoking it would have silently blinded every read skill. This is why the audit reads labels off /user/perms.php rather than trusting ids in code. The READ_ONLY baseline is now the audited read surface (35 rights), not a guess. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
This commit is contained in:
@@ -229,8 +229,21 @@ def stage_apply(args) -> None:
|
||||
if os.environ.get("ARCO_PROD_CONFIRM") != "I-UNDERSTAND-THIS-WRITES-PROD":
|
||||
die("set ARCO_PROD_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD to apply to production")
|
||||
|
||||
# The production writer is its OWN user with the narrow `prod-write` scope
|
||||
# (test/scopes.ts) — not the read-only `ai_agent` whose key every read skill
|
||||
# holds. A read credential must never be able to write the ledger, and the
|
||||
# writer must not be lying around in a file a dozen skills load.
|
||||
cfg = env_from(os.path.join(SKILL, "dolibarr", ".env"))
|
||||
base, key = cfg["DOLIBARR_URL"], cfg["DOLIBARR_API_KEY"]
|
||||
base = cfg["DOLIBARR_URL"]
|
||||
key_file = os.path.join(REPO, "test", ".ai_agent_prod_prod_write.key")
|
||||
if not os.path.exists(key_file):
|
||||
die("no production writer key.\n"
|
||||
f" expected: {key_file}\n"
|
||||
" provision it: DOLIBARR_ADDRESS=https://erp.arcodange.lab "
|
||||
"ARCO_ALLOW_PRODUCTION=erp.arcodange.lab "
|
||||
"ARCO_PROD_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD \\\n"
|
||||
" deno run -A test/provisionAiUser.ts --scope prod-write --env production")
|
||||
key = open(key_file).read().strip()
|
||||
if SANDBOX_HOST in base:
|
||||
die(f"the prod .env points at the sandbox ({base}) — nothing to promote to")
|
||||
print(f"*** PRODUCTION: {base} — approved by {gate['by']} at {gate['at']} ***")
|
||||
|
||||
Reference in New Issue
Block a user