Skip to content

dcstuff/Permishifizer9000

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 

Repository files navigation

Permishifizer 9000

Generate a SalesforceBackup permission set XML from live org metadata — ensuring your backup/restore integration user has access to every restorable object, field, and record type.


Table of Contents


Overview

permishifizer9000.py connects to an authenticated Salesforce org, introspects its full metadata surface, and writes a SalesforceBackup.permissionset-meta.xml file. When deployed and assigned to a user, this permission set grants the minimum access needed for comprehensive backup and restore operations across all standard and custom objects.

The permission set is always regenerated from scratch so it stays aligned with the current org schema — no manual upkeep required when objects, fields, or record types change.

What Gets Generated

Permission Type Scope Access Granted
objectPermissions All permissionable objects in the org allowRead=true, allowCreate=true
fieldPermissions All permissionable fields on those objects readable=true; editable=true for all permissionable fields
recordTypeVisibilities All record types on included objects (including PersonAccount) visible=true
userPermissions Configured user-level permissions in USER_PERMISSION_NAMES enabled=true

What Gets Excluded

The following categories are intentionally excluded because they are event streams, not restorable record data:

  • Change Data Capture objects — API names ending in ChangeEvent
  • Platform Event objects — API names ending in __e
  • Predefined exclusions — any API names listed in the EXCLUDED_OBJECT_API_NAMES set within the script
  • Compound component fields — component field APIs (for example BillingStateCode) are skipped because they are not valid in fieldPermissions; compatible non-component fields are used instead
  • Person Account alias fieldsAccount.*__pc fields (mirrors of Contact custom fields) are skipped to avoid duplicate entries

Prerequisites

Requirement Details
Python 3.12 or later
Salesforce CLI (sf) Installed and on PATH. Install guide
Authenticated org At least one org authenticated via sf org login web or sf org login sfdx-url
User Permissions The authenticated user requires:
API Enabled (ApiEnabled)
Customize Application (CustomizeApplication)
Manage Profiles and Permission Sets (ManageProfilesPermissionsets)
Modify Metadata Through Metadata API Functions (ModifyMetadata)

Note: No third-party Python packages are required — the script uses only the standard library.

Installation

Clone the repository (or copy the script) into your project:

git clone <repository-url> permishifizer9000
cd permishifizer9000

Usage

Quick Start

# Use the default target org (sf CLI default)
python permishifizer9000.py

# Recommended (explicit): specify an org alias
python permishifizer9000.py --target-org myOrgAlias
python permishifizer9000.py -o myOrgAlias

# Recommended (explicit): specify an org username
python permishifizer9000.py --target-org admin@example.com

# Convenience shorthand (also supported): positional target org
python permishifizer9000.py myOrgAlias

# Generate and automatically deploy the permission set
python permishifizer9000.py --target-org myOrgAlias --deploy
python permishifizer9000.py -o myOrgAlias -d

# Generate and deploy with a custom deploy wait window (minutes)
python permishifizer9000.py --target-org myOrgAlias --deploy --deploy-wait 20
python permishifizer9000.py -o myOrgAlias -d -w 20

# Custom output path using shorthand
python permishifizer9000.py -o myOrgAlias -f ./output/SalesforceBackup.permissionset-meta.xml

The generated permission set is written to:

./force-app/main/default/permissionsets/SalesforceBackup.permissionset-meta.xml

Deploy it immediately:

sf project deploy start --ignore-conflicts --source-dir "force-app/main/default/permissionsets/SalesforceBackup.permissionset-meta.xml"

Assign to a user:

sf org assign permset --name SalesforceBackup --target-org myOrgAlias

Command-Line Reference

usage: permishifizer9000.py [-h] [--target-org TARGET_ORG] [--output-file OUTPUT_FILE] [--deploy] [--deploy-wait DEPLOY_WAIT] [target_org]

positional arguments:
  target_org              Optional Salesforce org alias/username (convenience shorthand)

options:
  -h, --help              Show this help message and exit
  --target-org TARGET_ORG, -o TARGET_ORG
                          Salesforce org alias/username (preferred for CI/CD; same meaning as positional target_org)
  --output-file OUTPUT_FILE, -f OUTPUT_FILE
                          Output permission set XML path (default: force-app/main/default/permissionsets/SalesforceBackup.permissionset-meta.xml)
  --deploy, -d            Automatically deploy the generated permission set after writing it
  --deploy-wait DEPLOY_WAIT, -w DEPLOY_WAIT
                          Minutes to wait for deployment completion when using --deploy (default: 10). Must be >= 1.

Short-flag note: this project maps -o to --target-org and -f to --output-file.

Target Org Resolution

For automation and CI/CD, prefer the explicit --target-org flag for readability in pipeline logs and scripts. The positional argument remains supported as a convenience shorthand for interactive/local usage.

The script resolves the target org in the following priority order:

Priority Source Example
1 --target-org flag python permishifizer9000.py --target-org myOrg
2 Positional argument python permishifizer9000.py myOrg
3 SF_TARGET_ORG environment var export SF_TARGET_ORG=myOrg
4 Salesforce CLI default org Set via sf config set target-org=myOrg

Custom Output Path

Write the permission set to a different location:

python permishifizer9000.py --output-file ./output/BackupPerms.permissionset-meta.xml
python permishifizer9000.py -f ./output/BackupPerms.permissionset-meta.xml

Automatic Deployment

To deploy immediately after generation, add --deploy:

python permishifizer9000.py --target-org myOrg --deploy

When --deploy is used, the script runs sf project deploy start --ignore-conflicts for the generated permission set file.

Before any org/API processing starts, deploy mode performs a preflight validation and fails fast if either condition is not met:

  • sfdx-project.json can be located at project root
  • The generated output file path is inside one of the packageDirectories[].path roots from sfdx-project.json

This prevents long-running metadata generation when deployment cannot succeed due to project structure.

By default, the script waits up to 10 minutes for deployment completion. To change that (minimum 1 minute):

python permishifizer9000.py --target-org myOrg --deploy --deploy-wait 20

Excluding Additional Objects

Edit the EXCLUDED_OBJECT_API_NAMES set in the script to exclude specific objects from the generated permission set:

EXCLUDED_OBJECT_API_NAMES = {
    "Task",
    "Event",
    "CustomObject__c",
    "acme__Subscription__c",
}

All excluded objects are removed from objectPermissions, fieldPermissions, and recordTypeVisibilities.

Supplementing Object Permissions

Some objects are valid for objectPermissions but are not returned by the PicklistValueInfo query that the script uses in Step 1. Salesforce handles these objects specially and omits them from the picklist. Edit the OBJECT_PERMISSION_SUPPLEMENTAL_OBJECTS set in the script to ensure they are included:

OBJECT_PERMISSION_SUPPLEMENTAL_OBJECTS = {"Task", "Event"}

These names are merged into the Step 1 result set before the cross-reference against EntityDefinition, so they appear in the generated objectPermissions as long as they also exist as non-excluded objects in the org.

Supplementing Field Queries

Some objects have permissionable fields that are not indexed in the EntityParticle Tooling API table the script queries in Step 3, or are filtered out of the EntityParticle scan because EntityDefinition.IsFlsEnabled = false even though they have permissionable custom fields (for example User, whose standard fields are governed by user-management permissions rather than the <fieldPermissions> framework). For these objects, the script falls back to querying FieldDefinition one object at a time. Edit the FIELD_QUERY_SUPPLEMENTAL_OBJECTS list in the script to add or remove objects that need this treatment:

FIELD_QUERY_SUPPLEMENTAL_OBJECTS = [
    "EmailMessage",
    "User",
]

Fields returned for these objects are added to the generated fieldPermissions alongside the regular EntityParticle results.

Supplementing Individual Field Permissions

A small number of fields are valid in fieldPermissions (and selectable in the Salesforce permission set UI) but are not returned by either EntityParticle or FieldDefinition — for example DisputeItemChargeBack.CardBrand. Add fully-qualified field API names to FIELD_PERMISSION_SUPPLEMENTAL_FIELDS in the script to inject them verbatim:

FIELD_PERMISSION_SUPPLEMENTAL_FIELDS = {
    "DisputeItemChargeBack.CardBrand",
}

Each entry must use the Object.Field format. Entries whose parent object is excluded (event objects, predefined exclusions) are skipped.

Configuring User Permissions

Edit the USER_PERMISSION_NAMES set in the script to add/remove generated userPermissions entries:

USER_PERMISSION_NAMES = {
  "AllowViewEditConvertedLeads",
  "ModifyAllData",
  "ViewAllData",
}

Depending on your backup/restore setup, additional permissions may be required. For guidance, see Salesforce Help: https://help.salesforce.com/s/articleView?id=platform.backup_recover_o_auth_user_perms.htm&type=5

Each entry generates:

<userPermissions>
  <enabled>true</enabled>
  <name>AllowViewEditConvertedLeads</name>
</userPermissions>

Output

The script produces a standard Salesforce metadata XML file. A condensed example:

<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
    <description>Grants object, field, and record type access for full-record backup/restore scope (excluding ChangeEvent and __e event objects).</description>
    <hasActivationRequired>false</hasActivationRequired>
    <label>Salesforce Backup</label>
    <fieldPermissions>
        <editable>true</editable>
        <field>Account.Name</field>
        <readable>true</readable>
    </fieldPermissions>
    <!-- ... more fieldPermissions ... -->
    <objectPermissions>
        <allowCreate>true</allowCreate>
        <allowDelete>false</allowDelete>
        <allowEdit>false</allowEdit>
        <allowRead>true</allowRead>
        <modifyAllRecords>false</modifyAllRecords>
        <object>Account</object>
        <viewAllRecords>false</viewAllRecords>
    </objectPermissions>
    <!-- ... more objectPermissions ... -->
    <recordTypeVisibilities>
        <recordType>Account.MyRecordType</recordType>
        <visible>true</visible>
    </recordTypeVisibilities>
    <!-- ... more recordTypeVisibilities ... -->
     <userPermissions>
        <enabled>true</enabled>
        <name>AllowViewEditConvertedLeads</name>
    </userPermissions>
    <!-- ... more userPermissions ... -->
</PermissionSet>

Console Output

During execution the script prints a progress summary:

Connecting to Salesforce org...
Querying org metadata (this may take several minutes for large orgs)...

Step 1/4  Fetching permissionable object list from PicklistValueInfo...
         Found 412 permissionable objects.

Step 2/4  Fetching object list from EntityDefinition...
         Found 1247 objects (892 with FLS enabled).
         Excluded 38 event objects, 0 predefined objects, 355 non-FLS objects.
         Using 374 objects for objectPermissions.

Step 3/4  Querying EntityParticle in 25 batch(es) of up to 50 objects...
  [  1/ 25] Account – Contact
  ...

         Total permissionable fields: 8342

Step 4/4  Fetching record types via Metadata API...
         Found 67 record types from Metadata API.
         Using 54 record types in scope.

Permission set written: .../SalesforceBackup.permissionset-meta.xml
  objectPermissions:      374
  fieldPermissions:       8342
  recordTypeVisibilities: 54
  userPermissions:        52

Done!
Deploy with: sf project deploy start --ignore-conflicts --source-dir "..."

CI/CD Integration

The script is designed for automated pipelines. It has zero Python dependencies beyond the standard library and exits with meaningful error codes.

All CI examples below use python permishifizer9000.py --deploy, which already invokes sf project deploy start --ignore-conflicts internally. If you switch to a direct CLI deploy step, use sf project deploy start --ignore-conflicts --source-dir "...".

Exit Code Meaning
0 Success
1 Script error (auth, network, etc.)
130 Interrupted by user (Ctrl+C)

Azure DevOps Pipelines

trigger: none

schedules:
  - cron: "0 6 * * 1"
    displayName: Weekly Monday 06:00 UTC
    branches:
      include:
        - main

pool:
  vmImage: "ubuntu-latest"

steps:
  - task: UseNode@1
    inputs:
      version: "20.x"

  - task: UsePythonVersion@0
    inputs:
      versionSpec: "3.12"

  - script: npm install -g @salesforce/cli
    displayName: Install Salesforce CLI

  - script: |
      echo "$(SF_AUTH_URL)" > auth_url.txt
      sf org login sfdx-url --sfdx-url-file auth_url.txt --set-default --alias ci-org
      rm auth_url.txt
    displayName: Authenticate to Salesforce

  - script: python permishifizer9000.py --target-org ci-org --deploy --deploy-wait 10
    displayName: Generate and deploy permission set

  - script: sf org assign permset --name SalesforceBackup --target-org ci-org
    displayName: Assign permission set

GitHub Actions

name: Generate & Deploy Backup Permission Set

on:
  workflow_dispatch:
  schedule:
    - cron: "0 6 * * 1"  # Weekly on Monday at 06:00 UTC

jobs:
  generate-backup-perms:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install Salesforce CLI
        run: npm install -g @salesforce/cli

      - name: Authenticate to Salesforce
        run: |
          echo "${{ secrets.SF_AUTH_URL }}" > auth_url.txt
          sf org login sfdx-url --sfdx-url-file auth_url.txt --set-default --alias ci-org
          rm auth_url.txt

      - name: Generate and deploy permission set
        run: python permishifizer9000.py --target-org ci-org --deploy --deploy-wait 10

      - name: Assign permission set
        run: |
          sf org assign permset \
            --name SalesforceBackup \
            --target-org ci-org

GitLab CI

generate-backup-perms:
  image: node:20
  stage: deploy
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
    - if: $CI_PIPELINE_SOURCE == "web"
  before_script:
    - npm install -g @salesforce/cli
    - apt-get update && apt-get install -y python3
    - echo "$SF_AUTH_URL" > auth_url.txt
    - sf org login sfdx-url --sfdx-url-file auth_url.txt --set-default --alias ci-org
    - rm auth_url.txt
  script:
    - python3 permishifizer9000.py --target-org ci-org --deploy --deploy-wait 10
    - sf org assign permset --name SalesforceBackup --target-org ci-org

Bitbucket Pipelines

pipelines:
  custom:
    generate-backup-perms:
      - step:
          name: Generate & Deploy Backup Permission Set
          image: node:20
          script:
            - npm install -g @salesforce/cli
            - apt-get update && apt-get install -y python3
            - echo "$SF_AUTH_URL" > auth_url.txt
            - sf org login sfdx-url --sfdx-url-file auth_url.txt --set-default --alias ci-org
            - rm auth_url.txt
            - python3 permishifizer9000.py --target-org ci-org --deploy --deploy-wait 10
            - sf org assign permset --name SalesforceBackup --target-org ci-org

Jenkins (Declarative Pipeline)

pipeline {
    agent any
    triggers {
        cron('H 6 * * 1')  // Weekly on Monday
    }
    environment {
        SF_AUTH_URL = credentials('sf-auth-url')
    }
    stages {
        stage('Setup') {
            steps {
                sh 'npm install -g @salesforce/cli'
            }
        }
        stage('Authenticate') {
            steps {
                sh '''
                    echo "$SF_AUTH_URL" > auth_url.txt
                    sf org login sfdx-url --sfdx-url-file auth_url.txt --set-default --alias ci-org
                    rm auth_url.txt
                '''
            }
        }
        stage('Generate') {
            steps {
            sh 'python3 permishifizer9000.py --target-org ci-org --deploy --deploy-wait 10'
            }
        }
        stage('Assign') {
            steps {
                sh 'sf org assign permset --name SalesforceBackup --target-org ci-org'
            }
        }
    }
}

Environment Variable Authentication (All Platforms)

If you prefer environment-based org targeting without explicit --target-org flags:

export SF_TARGET_ORG="ci-org"
python permishifizer9000.py

The script picks up SF_TARGET_ORG automatically when no positional or flag argument is provided.

How It Works

The script executes four sequential phases against the target org's APIs:

┌──────────────────────────────────────────────────────────────────────┐
│ Step 1 — PicklistValueInfo (REST API)                                │
│ Fetches all permissionable object names from the ObjectPermissions   │
│ SobjectType picklist. Uses ordered keyset pagination (`ORDER BY` +   │
│ `Value > lastValue`) in pages of 2,000 rows. Merges in any names     │
│ from `OBJECT_PERMISSION_SUPPLEMENTAL_OBJECTS` that Salesforce omits  │
│ from the picklist (e.g. Task, Event).                                │
├──────────────────────────────────────────────────────────────────────┤
│ Step 2 — EntityDefinition (Tooling API)                              │
│ Fetches all non-deprecated object names and FLS status via ordered   │
│ keyset paging (`QualifiedApiName > lastValue`), excludes event,      │
│ predefined, and non-FLS objects, and cross-references Step 1 to keep │
│ only concrete objects in scope for objectPermissions.                │
├──────────────────────────────────────────────────────────────────────┤
│ Step 3 — EntityParticle (Tooling API)                                │
│ Queries all permissionable fields in batches of 50 objects.          │
│ Marks included fields as editable in the generated permission set.   │
│ Excludes compound component fields (IsComponent=true), Person        │
│ Account alias fields (Account.*__pc), and any fields belonging to    │
│ predefined-excluded objects as a final guard. Objects listed in      │
│ `FIELD_QUERY_SUPPLEMENTAL_OBJECTS` are absent from EntityParticle;   │
│ their fields are fetched individually via FieldDefinition instead.   │
├──────────────────────────────────────────────────────────────────────┤
│ Step 4 — Metadata API (SOAP listMetadata)                            │
│ Retrieves all RecordType full names. Filters to objects in scope     │
│ and normalizes PersonAccount record types.                           │
└──────────────────────────────────────────────────────────────────────┘
                               │
                               ▼
              Writes SalesforceBackup.permissionset-meta.xml

Key Design Decisions

  • Full regeneration — The XML file is always written from scratch, not patched, ensuring the output always reflects the current org state.
  • Keyset pagination — Large object surfaces are fetched with deterministic, ordered pagination (ORDER BY + > lastValue) in QUERY_PAGE_SIZE chunks, reducing query count and complexity.
  • Permissionable-first field access — Any field returned as IsPermissionable=true is emitted with readable=true and editable=true to maximize backup/restore coverage, including formula and other non-writable field types.
  • Compound compatibility guardEntityParticle.IsComponent=true fields are excluded so generated fieldPermissions only contain valid field names Salesforce accepts during deployment.
  • PersonAccount normalizationAccount.PersonAccount record types are normalized to PersonAccount.PersonAccount as required by permission set XML format.
  • No external dependencies — Uses only the Python standard library (urllib, xml.etree, json, subprocess, etc.) to simplify CI/CD setup.

Troubleshooting

Symptom Cause Fix
Error: Salesforce CLI (sf) was not runnable sf not installed or not on PATH Install the Salesforce CLI and restart your shell
Error: Could not parse org display output Org not authenticated or token expired Re-authenticate: sf org login web --alias myOrg
Error: \sf org auth show-access-token --json` failed` Salesforce CLI is outdated and lacks the show-access-token subcommand Update the CLI: sf update (or reinstall from https://developer.salesforce.com/tools/salesforcecli)
Error: HTTP 401 during REST query Access token expired mid-run Re-authenticate and re-run
Error: No objects returned Org connectivity issue or insufficient permissions Verify the authenticated user has API and metadata access
Error: Timeout after 90s Slow org or network Retry; consider running closer to the Salesforce instance
Error: Conflicting target org values Both positional and --target-org provided with different values Use one or the other, not both with different values
Error: Python 3.12+ is required Older Python version detected Upgrade Python to 3.12 or later
Error: --deploy requires sfdx-project.json at the project root Deploy mode could not find sfdx-project.json from output path/cwd/script directory Run from a Salesforce DX project or provide an output path inside that project
Error: --deploy requires the generated metadata file to be within a configured package directory --output-file is outside all packageDirectories[].path entries in sfdx-project.json Use an output path under a configured package directory or update packageDirectories
Script runs but output has fewer objects than expected Objects may be in EXCLUDED_OBJECT_API_NAMES or are event types Review exclusions in the script; check console output for exclusion counts

Configuration Reference

These constants can be modified at the top of permishifizer9000.py:

Constant Default Description
PERMSET_NAME "SalesforceBackup" API name of the generated permission set
PERMSET_LABEL "Salesforce Backup" Display label of the permission set
PERMSET_DESC (see source) Description embedded in the permission set XML
DEFAULT_PERMSET_FILE ./force-app/main/.../SalesforceBackup.permissionset-meta.xml Default output path
EXCLUDED_OBJECT_API_NAMES set() (empty) Set of object API names to exclude from objectPermissions, fieldPermissions, and recordTypeVisibilities (does not affect userPermissions)
OBJECT_PERMISSION_SUPPLEMENTAL_OBJECTS {"Task", "Event"} Objects valid for objectPermissions but missing from the PicklistValueInfo query; merged into the Step 1 result set before cross-referencing
FIELD_QUERY_SUPPLEMENTAL_OBJECTS ["EmailMessage", "User"] Objects whose permissionable fields are not indexed in EntityParticle (or whose parent object reports IsFlsEnabled = false, e.g. User); their fields are fetched individually via FieldDefinition at the end of Step 3
FIELD_PERMISSION_SUPPLEMENTAL_FIELDS {"DisputeItemChargeBack.CardBrand"} Fully-qualified field API names (Object.Field) injected verbatim into fieldPermissions; use for fields valid in permission sets but absent from both EntityParticle and FieldDefinition
USER_PERMISSION_NAMES (see source) Set of user permission API names emitted as userPermissions entries
BATCH_SIZE 50 Number of objects per EntityParticle query batch
QUERY_PAGE_SIZE 2000 Page size for PicklistValueInfo and EntityDefinition keyset pagination
HTTP_TIMEOUT_SECONDS 90 Timeout for each HTTP request to Salesforce
SF_COMMAND_TIMEOUT_SECONDS 120 Timeout for Salesforce CLI subprocess calls
DEPLOY_TIMEOUT_BUFFER_SECONDS 300 Extra seconds added to deploy_wait × 60 for the deploy subprocess timeout
MIN_PYTHON_VERSION (3, 12) Minimum required Python version; script exits if the runtime is older
TARGET_ORG_ENV_VAR "SF_TARGET_ORG" Environment variable checked for target org fallback

About

Generate a "Salesforce Backup" permission set XML from live org metadata — ensuring your backup/restore integration user has access to every restorable object, field, and record type.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages