#!/usr/bin/env python3 """Regenerate ``seeds/credits-permissions.seed.json`` from a read-only prod dump of the 5 Credits permission sets (#3880, Model-3 pilot). This is the tracked, reviewable derivation behind the committed seed's ``source.generator`` claim — the seed is generated, not hand-authored. It encodes the reviewed policy decisions, so changing the permission matrix means editing this script and re-running it, not hand-editing the JSON. Input (``--dump``): a JSON file with ``permission_sets`` / ``object_permissions`` / ``field_permissions``, each a list of the raw prod rows (including ``Id``) for the 5 ``permset_credits_*`` permsets on ``Credits__c`` / ``Credits_Topup__c``. Produce it with a read-only SELECT of those three tables filtered to the Credits permsets (see docs/object-proposals/poc-credits-refactor.md for the matrix). Transforms applied (each grounded in a contract): - existing rows keep their prod ids (adopted by PRIMARY key); new rows get minted ``*_pilot`` ids; - ObjectPermissions are Credits__c-only (Topup is a ControlledByParent child); - ModifyAll cascades to ViewAll/Edit/Delete/Read (Salesforce/730_AC2), so Sales Ops = full CRUD incl. delete (decision 2026-07-20); - explicit read=0 DENY FieldPermissions rows are preserved and mirrored onto Topup (Initial_Credits__c -> Topup_Amount__c, Credit_Lifecycle__c dropped), with edit capped 0 on the Topup formula field Requires_Finance_Approval__c; - Regional_Ops_User__c read FLS added to Credits_Sales_Manager on both objects (#3921 forcing item). Usage: ``python3 gen_credits_permissions_seed.py --dump `` """ from __future__ import annotations import argparse import json from collections import Counter from pathlib import Path SALES_MANAGER = "permset_credits_sales_manager_manual_20260531" # Pilot scope (#3880): exactly these 5 Credits permsets, on these 2 objects. # Pinned by id AND name — a count check alone would accept five foreign permsets # and emit them into the deploy seed as an authorization mis-target. EXPECTED_PERMSETS = { "permset_credits_ae_manual_20260531": "Credits_AE", "permset_credits_finance_ops_manual_20260531": "Credits_Finance_Ops", "permset_credits_marketing_read_manual_20260531": "Credits_Marketing_Read", SALES_MANAGER: "Credits_Sales_Manager", "permset_credits_sales_ops_manual_20260531": "Credits_Sales_Ops", } TARGET_OBJECTS = frozenset({"Credits__c", "Credits_Topup__c"}) # Credits__c field -> Credits_Topup__c field (None = no Topup equivalent, drop). FIELD_MAP = { "Approval_Status__c": "Approval_Status__c", "Requires_Finance_Approval__c": "Requires_Finance_Approval__c", "Credit_Lifecycle__c": None, "Initial_Credits__c": "Topup_Amount__c", } # Topup fields that are formula/non-updateable — an edit grant there is inert but # wrong, so cap edit=0 when mirroring. TOPUP_READONLY = {"Requires_Finance_Approval__c"} def short(parent: str) -> str: return parent.replace("permset_credits_", "").replace("_manual_20260531", "") def field_leaf(field: str) -> str: return field.split(".", 1)[1] if "." in field else field def cascade_object_flags(read, create, edit, delete, view_all, modify_all): """Platform/Salesforce ObjectPermissions cascade (730_AC2): ModifyAll implies ViewAll/Edit/Delete, any granted flag implies Read. Never removes a grant.""" if modify_all: view_all = edit = delete = 1 if any((create, edit, delete, view_all, modify_all)): read = 1 return read, create, edit, delete, view_all, modify_all def validate_generated(permsets: list[dict], objp: list[dict], fldp: list[dict]) -> None: """Fail closed on a dump that does not describe the expected pilot scope. The generator is only meaningful for the 5 Credits permsets on the 2 Credits objects; a dump that is short, over-broad, or carries a foreign permset would otherwise silently produce a seed that grants or drops the wrong things. """ got = {p["id"]: p["name"] for p in permsets} if got != EXPECTED_PERMSETS: missing = sorted(set(EXPECTED_PERMSETS) - set(got)) unexpected = sorted(set(got) - set(EXPECTED_PERMSETS)) renamed = sorted( f"{i}: expected {EXPECTED_PERMSETS[i]!r}, got {got[i]!r}" for i in set(got) & set(EXPECTED_PERMSETS) if got[i] != EXPECTED_PERMSETS[i] ) raise SystemExit( "dump does not describe the expected Credits pilot permission sets — " f"missing={missing} unexpected={unexpected} renamed={renamed}" ) known = set(EXPECTED_PERMSETS) for label, rows in (("objectPermissions", objp), ("fieldPermissions", fldp)): for row in rows: if row["parentId"] not in known: raise SystemExit( f"{label} row {row['id']!r} has parentId {row['parentId']!r} " f"outside the {len(EXPECTED_PERMSETS)} seeded permission sets" ) if row["sobjectType"] not in TARGET_OBJECTS: raise SystemExit( f"{label} row {row['id']!r} targets {row['sobjectType']!r}, " f"outside {sorted(TARGET_OBJECTS)}" ) ids = [row["id"] for row in rows] if len(ids) != len(set(ids)): dupes = sorted(i for i, n in Counter(ids).items() if n > 1) raise SystemExit(f"duplicate {label} ids: {dupes}") def build_seed(dump: dict) -> dict: # Source of truth is the Credits__c permset matrix (the 007 rows). Ignore any # rows the seed itself adds (Credits_Topup__c mirror + minted *_pilot rows) so # re-running against a dump taken AFTER this seed was applied is idempotent and # never emits a Topup row as Credits__c or a duplicate id. def is_source(row: dict) -> bool: return row["SobjectType"] == "Credits__c" and not str(row["Id"]).endswith("_pilot") out_permsets = [{ "id": p["Id"], "name": p["Name"], "label": p["Label"], "description": p.get("Description") or "", "isActive": int(p["IsActive"] or 0), "isCustom": int(p["IsCustom"] or 0), } for p in sorted(dump["permission_sets"], key=lambda x: x["Name"])] out_objp = [] for o in dump["object_permissions"]: if not is_source(o): continue r, c, e, dl, va, ma = cascade_object_flags( int(o["PermissionsRead"]), int(o["PermissionsCreate"]), int(o["PermissionsEdit"]), int(o["PermissionsDelete"]), int(o["PermissionsViewAllRecords"]), int(o["PermissionsModifyAllRecords"])) out_objp.append({ "id": o["Id"], "parentId": o["ParentId"], "sobjectType": "Credits__c", "read": r, "create": c, "edit": e, "delete": dl, "viewAll": va, "modifyAll": ma, }) source_fields = [f for f in dump["field_permissions"] if is_source(f)] out_fldp = [] for f in source_fields: # Credits__c verbatim (prod id), deny rows kept out_fldp.append({ "id": f["Id"], "parentId": f["ParentId"], "sobjectType": "Credits__c", "field": f["Field"], "read": int(f["PermissionsRead"]), "edit": int(f["PermissionsEdit"]), }) for f in source_fields: # Topup mirror (minted ids) leaf = field_leaf(f["Field"]) tgt = FIELD_MAP.get(leaf, leaf) if tgt is None: continue edit = 0 if tgt in TOPUP_READONLY else int(f["PermissionsEdit"]) out_fldp.append({ "id": f"fieldperm_credits_topup_{short(f['ParentId'])}_{tgt.replace('__c','')}_pilot", "parentId": f["ParentId"], "sobjectType": "Credits_Topup__c", "field": f"Credits_Topup__c.{tgt}", "read": int(f["PermissionsRead"]), "edit": edit, }) for obj in ("Credits__c", "Credits_Topup__c"): # Regional_Ops FLS (#3921) tag = "credits" if obj == "Credits__c" else "topup" out_fldp.append({ "id": f"fieldperm_credits_sm_regionalops_{tag}_pilot", "parentId": SALES_MANAGER, "sobjectType": obj, "field": f"{obj}.Regional_Ops_User__c", "read": 1, "edit": 0, }) # Deterministic order: regeneration must produce a byte-stable file so a # re-run diffs cleanly against the committed seed (dump row order is not # guaranteed). Sort by natural key, which is also how a reviewer reads it. out_objp.sort(key=lambda r: (r["parentId"], r["sobjectType"])) out_fldp.sort(key=lambda r: (r["parentId"], r["sobjectType"], r["field"])) validate_generated(out_permsets, out_objp, out_fldp) return { "version": 1, "description": "Credits permission matrix (Model-3 pilot, #3880). Generated from a " "read-only prod dump. Definitions only (PermissionSet/ObjectPermissions/" "FieldPermissions), never PermissionSetAssignment. Existing rows carry prod " "ids and are adopted by PRIMARY key (upsert, IsDeleted=0 reactivates); the " "applier is additive/upsert-only. ObjectPermissions are Credits__c-only " "(Topup is a ControlledByParent child); explicit read=0 DENY rows are " "preserved and mirrored onto Topup; Sales Ops ModifyAll cascades to full " "CRUD incl. delete (decision 2026-07-20); Regional_Ops_User__c read FLS added " "to Credits_Sales_Manager on both objects (#3921 forcing item).", "source": {"env": "prod", "generator": "gen_credits_permissions_seed.py"}, "permissionSets": out_permsets, "objectPermissions": out_objp, "fieldPermissions": out_fldp, } def main() -> int: here = Path(__file__).resolve().parent parser = argparse.ArgumentParser(description="Regenerate the Credits permission seed.") parser.add_argument("--dump", required=True, type=Path, help="read-only prod dump JSON (permission_sets/object_permissions/field_permissions)") parser.add_argument("--out", type=Path, default=here / "seeds" / "credits-permissions.seed.json") args = parser.parse_args() seed = build_seed(json.loads(args.dump.read_text())) args.out.write_text(json.dumps(seed, indent=2, ensure_ascii=False) + "\n") print(f"wrote {args.out}") print(f" permissionSets={len(seed['permissionSets'])} " f"objectPermissions={len(seed['objectPermissions'])} " f"fieldPermissions={len(seed['fieldPermissions'])}") return 0 if __name__ == "__main__": raise SystemExit(main())