PermDock
CLI

diff

Compare two policies or catalogs, list the permissions, roles, scopes and grants that changed, run fixtures through both with --impact, and exit 1 on a breaking change.

permdock diff a b reads two versions of a policy and reports what changed between them: permissions, scopes, roles, and every grant. It classifies each change as breaking or not and exits 1 when any is breaking, so a CI step can hold a pull request that takes access away without anyone reading the TypeScript diff. With --impact it also runs the project's fixtures through both policies and prints who loses or gains what.

Usage

permdock diff permissions.catalog.json src/policy.ts          # committed catalog vs the working tree
permdock diff src/policy.ts src/policy.next.ts --json           # two modules, machine-readable
permdock diff src/policy.ts src/policy.next.ts --impact         # plus the fixtures' outcomes
permdock diff a.catalog.json b.catalog.json                     # two catalogs from two commits

Each input is either a permissions.catalog.json (any file ending in .json, validated with parseCatalog) or a module that exports policy, which the command turns into a catalog in memory with buildCatalog. A catalog written by permdock collect with policy configured carries a grants section (catalog); one written without the policy does not, and diff then compares permissions, scopes and roles only and says so.

FlagMeaning
--impactLoad the fixtures and evaluate each one against both policies; both inputs must be modules. A fixture that was granted and no longer is counts as breaking (access-lost)
--fixtures <file>The fixture file for --impact; defaults to rls.fixtures in the config, then rls.fixtures.json. The format is the one rls verify reads: { subject: { id, roles?, tenant?, memberships? }, row, newRow?, action }
--jsonPrint the report as JSON instead of text

What is breaking

A change is breaking when it can take a decision from granted to something else for some subject. The classification reads the grants' structure and does not evaluate rows, so it is conservative: a condition that changed is reported as narrowing even when it widened, because without data the command cannot tell. --impact is the precise answer for the subjects and rows the fixtures name.

KindTrigger
permission-removedA permission key in a is absent from b
alias-removedA key a lists in renamedFrom is neither a key nor a renamedFrom entry in b: stored custom roles, scopes and SQL that still name it now deny
level-removedA level a lists on a permission is gone from that permission in b: stored custom roles that pick it now deny the permission (levels)
scope-removedA declared scope in a is absent from b
role-removedA role in a is absent from b; its grants are not listed again
allow-removedAn allow with no counterpart in b (same permission, role, grantee and scope), on a permission and role that still exist
allow-narrowedAn allow whose counterpart gained or changed a where or check, gained or changed an approval, lost fields, started later or ends earlier (validFrom / validUntil), gained or changed a limit, changed purpose, or became non-portable
deny-addedA deny in b with no counterpart in a
deny-changedA deny whose body changed in any way, since a looser deny condition denies more rows
delegation-removedA policy delegation in a (same from and to) is absent from b; the agents it covered lose every delegated permission
delegation-narrowedA delegation whose counterpart lost permission keys or whose validity starts later or ends earlier
access-lostWith --impact: a fixture that was granted under a and is denied or approval-required under b

Not breaking: a renamed permission, whose old key b lists in renamedFrom (a's grants are compared under the new key, and the text form prints old → new (renamed)); an added permission, level, scope, role, allow or delegation; a removed deny; an approval or condition removed from an allow; a wider field list or validity window; more permission keys on a delegation; a role declaration change (assignable, assigns, min, max and so on), which is listed under roles as ~ name: declaration changed but is not an access change on its own.

Output

The text form lists each section with + for added, - for removed and ~ for changed entries, then impact when requested and breaking (n) with one line per breaking change:

roles
  - auditor
  ~ admin: declaration changed
grants
  + allow post.archive (role member)
  + deny post.read (role member)
  - allow post.publish (role member)
  ~ allow post.delete (role member): approval added
impact (2 fixture(s) change outcome)
  u1 post.publish: granted → denied
  u1 post.delete: granted → approval-required
breaking (5)
  role-removed: role auditor no longer exists
  allow-removed: allow post.publish (role member) removed
  deny-added: deny post.read (role member) added
  allow-narrowed: allow post.delete (role member): approval added
  access-lost: u1 loses post.publish: granted → denied

--json prints { a, b, permissions, levels, scopes, roles, grants?, delegations?, impact?, breaking }: permissions has added, removed and renamed ({ from, to }); levels has added and removed as { permission, level } for permissions on both sides, printed as key@level; a and b carry each input's path and catalog fingerprint; grants.added and grants.removed are catalog grant entries, grants.changed pairs before and after with the list of changes; delegations has the same three lists over the catalog's delegations section and is present whenever grants is; impact rows are { action, subject, tenant?, before, after }; breaking entries are { kind, permission?, role?, detail }.

Exit codes

CodeMeaning
0No breaking change (there may be non-breaking ones)
1At least one breaking change, including access-lost from --impact
2Usage error: fewer or more than two inputs, a missing file, an invalid catalog, --impact over a catalog file, or unreadable fixtures

Why

Every other command reads one policy; a review needs two. The catalog already is the policy's JSON twin that CI commits, and reviewers were reading its diff by hand, so the command compares catalogs rather than inventing a second format, and the catalog gained grants and delegations sections so that the diff could see conditions, approvals, validity and standing delegations instead of only keys and role names. Breaking is defined structurally rather than by evaluation because a diff must run with no subjects, rows or stores on a build machine; the conservative rule ("a changed condition may have narrowed") errs toward holding the pull request, and --impact with the project's own fixtures gives the exact answer when the structure alone is ambiguous. The fixture format is the one rls verify already uses so a team writes its subjects and rows once. The command never evaluates a real subject, like the rest of the CLI (CLI).

Last updated on

On this page