fix(security): production read agent is now actually read-only #81

Merged
arcodange merged 1 commits from arcodange/prod-apply into main 2026-08-09 19:33:27 +02:00
2 changed files with 87 additions and 18 deletions
Showing only changes of commit 1cba032512 - Show all commits
+14 -1
View File
@@ -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']} ***")
+73 -17
View File
@@ -41,27 +41,79 @@ export interface Scope {
* `provisionAiUser.ts --audit` prints the live labels next to each granted id.
*/
export const PERMISSION_LABELS: Readonly<Record<number, string>> = {
// --- lecture / export (le socle read) ---
11: "Lire les factures clients",
16: "Exporter les factures clients",
21: "Consulter les propositions commerciales",
28: "Exporter les propositions commerciales",
31: "Lire les comptes bancaires",
41: "Lire les projets et les tâches",
45: "Exporter les projets",
91: "Lire les charges fiscales ou sociales et paiement de TVA",
94: "Exporter les charges fiscales ou sociales",
111: "Lire les factures fournisseurs",
121: "Consulter les tiers",
126: "Exporter les tiers",
141: "Lire tous les projets et tâches",
161: "Lire les contrats/abonnements",
167: "Exporter les contrats",
251: "Lire les utilisateurs",
262: "Étendre l'accès à TOUS les tiers et leurs objets (voir_tous)",
281: "Consulter les contacts",
358: "Exporter les utilisateurs",
531: "Consulter les services",
771: "Lire les notes de frais",
779: "Exporter les notes de frais",
1181: "Consulter les fournisseurs",
1182: "Consulter les commandes fournisseurs",
1191: "Exporter les commandes fournisseurs",
1201: "Récupérer le résultat d'un export",
1231: "Lire les factures (et paiements) fournisseurs",
1236: "Exporter les factures fournisseur",
1321: "Exporter les factures clients, attributs et règlements",
2401: "Lire ses propres actions/événements",
2411: "Lire les actions/événements des autres",
2414: "Exporter les événements des autres",
2501: "Lire/récupérer les documents",
3201: "Lire les événements archivés et leurs empreintes",
50411: "Lire les opérations du Grand livre",
// --- écriture (jamais dans le scope read) ---
12: "Créer/modifier les factures clients",
14: "Émettre des paiements sur les factures clients",
// 15 = "Supprimer les factures clients" — DELIBERATELY NEVER GRANTED
16: "Exporter les factures clients",
121: "Consulter les tiers",
122: "Créer/modifier les tiers",
// 125 = "Supprimer les tiers" — DELIBERATELY NEVER GRANTED
126: "Exporter les tiers",
241: "Consulter les propositions commerciales",
242: "Créer/modifier les propositions commerciales",
// 244 = "Supprimer les propositions commerciales" — DELIBERATELY NEVER GRANTED
261: "Consulter les produits/services",
262: "Créer/modifier les produits/services",
31: "Lire les comptes bancaires",
22: "Créer/modifier les propositions commerciales",
32: "Créer/modifier les comptes bancaires",
111: "Lire les factures fournisseurs",
251: "Lire les utilisateurs",
122: "Créer/modifier les tiers",
130: "Créer/modifier les informations de paiement des tiers",
282: "Créer/modifier les contacts",
1202: "Créer/modifier un export",
1232: "Créer les factures fournisseur",
2402: "Créer/modifier des actions/événements",
2503: "Soumettre ou supprimer des documents",
50401: "Lier les produits et factures avec des comptes comptables",
// --- suppression : JAMAIS accordée par aucun scope ---
// 15 Supprimer les factures clients
// 27 Supprimer les propositions commerciales
// 125 Supprimer les tiers
// 2403 Supprimer des actions/événements
} as const;
const READ_ONLY: ReadonlyArray<number> = [11, 111, 121, 126, 241, 261, 31, 251, 16];
/**
* Le socle de lecture établi par audit de l'instance de production, pas
* deviné : c'est l'ensemble exact des droits de lecture/export dont les skills
* se servent aujourd'hui (factures clients et fournisseurs, tiers, contacts,
* documents, contrats, projets, banque, grand livre, TVA).
*
* `262` (voir_tous) est indispensable : sans lui, les endpoints de liste
* renvoient des tableaux vides plutôt qu'un 403 le piège documenté par la
* skill `dolibarr`.
*/
const READ_ONLY: ReadonlyArray<number> = [
11, 16, 21, 28, 31, 41, 45, 91, 94, 111, 121, 126, 141, 161, 167, 251, 262,
281, 358, 531, 771, 779, 1181, 1182, 1191, 1201, 1231, 1236, 1321, 2401,
2411, 2414, 2501, 3201, 50411,
];
export const SCOPES: Readonly<Record<string, Scope>> = {
/**
@@ -83,7 +135,7 @@ export const SCOPES: Readonly<Record<string, Scope>> = {
"sandbox-write": {
purpose: "Répétition d'un change-set sur la sandbox (factures, tiers, produits, propositions, règlements)",
environments: ["sandbox"],
rights: [...READ_ONLY, 12, 122, 262, 242, 14, 32],
rights: [...READ_ONLY, 12, 14, 22, 32, 122, 130, 282, 1232, 2503],
},
/**
@@ -98,7 +150,11 @@ export const SCOPES: Readonly<Record<string, Scope>> = {
"prod-write": {
purpose: "Écriture de production, gated — création/modification de factures et rattachement de règlements",
environments: ["production"],
rights: [...READ_ONLY, 12, 14],
// 2503 est nécessaire à builddoc (régénérer le PDF d'une facture modifiée).
// Dolibarr le livre en bundle « soumettre OU supprimer » : on ne peut pas
// avoir l'un sans l'autre. C'est la seule capacité de suppression du modèle,
// et elle est confinée au writer de production, gated par le promote.
rights: [...READ_ONLY, 12, 14, 2503],
},
} as const;